🛸 Sync 2026-10-18 10:48:36

This commit is contained in:
github-actions[bot] committed 2026-10-18 10:48:36 +08:00
1 parent 61e7ec8357
commit 2a4fb97efd
21 files changed
+1217 -823

No files matched your search

@@ -85,7 +85,9 @@ const PodmanFormContainer = podmanView.form.extend({
field.value(name, name);
}
});
field.description = _('Add the container to an existing pod. Network, hostname and port mappings come from the pod\'s infra container.');
field.description = _(
'Add the container to an existing pod. Network, hostname and port mappings come from the pod\'s infra container.'
);
field = this.section.option(form.Value, 'command', _('Command'));
field.placeholder = '/bin/sh';
@@ -115,7 +117,8 @@ const PodmanFormContainer = podmanView.form.extend({
field.placeholder = '/host/path:/container/path:ro\nvolume-name:/data';
field.rows = 4;
field.optional = true;
field.description = _('One per line. Format: source:destination[:options]. Options: ro, rw, Z, z');
field.description = _(
'One per line. Format: source:destination[:options]. Options: ro, rw, Z, z');
field = this.section.option(form.ListValue, 'network', _('Network'));
field.depends('pod', '');
@@ -254,15 +257,20 @@ const PodmanFormContainer = podmanView.form.extend({
const opts = parts.length > 2 ? parts[2].split(',') : [];
if (parts[0].includes('/')) {
const mount = { Source: parts[0], Destination: parts[1] };
if (opts.includes('ro')) mount.ReadOnly = true;
const selinux = opts.filter((o) => o === 'Z' || o === 'z');
if (selinux.length > 0) mount.options = selinux;
const mount = {
Type: 'bind',
Source: parts[0],
Destination: parts[1]
};
if (opts.length > 0) mount.Options = opts;
mounts.push(mount);
return;
}
const vol = { Name: parts[0], Dest: parts[1] };
const vol = {
Name: parts[0],
Dest: parts[1]
};
if (opts.length > 0) vol.Options = opts;
volumes.push(vol);
});
@@ -271,11 +279,17 @@ const PodmanFormContainer = podmanView.form.extend({
}
if (!data.pod) {
if (data.network === 'host') {
spec.netns = { nsmode: 'host' };
spec.netns = {
nsmode: 'host'
};
} else if (data.network === 'none') {
spec.netns = { nsmode: 'none' };
spec.netns = {
nsmode: 'none'
};
} else if (data.network && data.network !== 'bridge') {
spec.networks = { [data.network]: {} };
spec.networks = {
[data.network]: {}
};
}
}
if (data.restart !== 'no') spec.restart_policy = data.restart;
@@ -304,11 +318,15 @@ const PodmanFormContainer = podmanView.form.extend({
if (data.cpus || data.memory) {
spec.resource_limits = {};
if (data.cpus) {
spec.resource_limits.cpu = { quota: parseFloat(data.cpus) * 100000 };
spec.resource_limits.cpu = {
quota: parseFloat(data.cpus) * 100000
};
}
if (data.memory) {
const memBytes = podmanUtil.format.parseMemory(data.memory);
if (memBytes > 0) spec.resource_limits.memory = { limit: memBytes };
if (memBytes > 0) spec.resource_limits.memory = {
limit: memBytes
};
}
}
@@ -328,7 +346,7 @@ const PodmanFormContainer = podmanView.form.extend({
if (shouldStart) await container.start();
};
return this.super('handleCreate', [ createFn, _('Container') ]);
return this.super('handleCreate', [createFn, _('Container')]);
},
});
@@ -135,7 +135,9 @@ const Container = Model.base.extend({
},
getStateBadge() {
return E('div', { class: `badge ${this.getState()}` }, [ this.getState() ]);
return E('div', {
class: `badge ${this.getState()}`
}, [this.getState()]);
},
getAutoUpdateLabel() {
@@ -164,7 +166,9 @@ const Container = Model.base.extend({
const networkSettings = this.NetworkSettings || {};
const mounts = this.Mounts || [];
const spec = { image: imageRef };
const spec = {
image: imageRef
};
// Name (strip leading slash added by Podman inspect)
if (this.Name) spec.name = this.Name.replace(/^\//, '');
@@ -179,12 +183,36 @@ const Container = Model.base.extend({
if (config.Hostname) spec.hostname = config.Hostname;
if (config.StopSignal) {
const sigMap = {
SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGTRAP: 5,
SIGABRT: 6, SIGBUS: 7, SIGFPE: 8, SIGKILL: 9, SIGUSR1: 10,
SIGSEGV: 11, SIGUSR2: 12, SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15,
SIGCHLD: 17, SIGCONT: 18, SIGSTOP: 19, SIGTSTP: 20, SIGTTIN: 21,
SIGTTOU: 22, SIGURG: 23, SIGXCPU: 24, SIGXFSZ: 25, SIGVTALRM: 26,
SIGPROF: 27, SIGWINCH: 28, SIGIO: 29, SIGPWR: 30, SIGSYS: 31,
SIGHUP: 1,
SIGINT: 2,
SIGQUIT: 3,
SIGILL: 4,
SIGTRAP: 5,
SIGABRT: 6,
SIGBUS: 7,
SIGFPE: 8,
SIGKILL: 9,
SIGUSR1: 10,
SIGSEGV: 11,
SIGUSR2: 12,
SIGPIPE: 13,
SIGALRM: 14,
SIGTERM: 15,
SIGCHLD: 17,
SIGCONT: 18,
SIGSTOP: 19,
SIGTSTP: 20,
SIGTTIN: 21,
SIGTTOU: 22,
SIGURG: 23,
SIGXCPU: 24,
SIGXFSZ: 25,
SIGVTALRM: 26,
SIGPROF: 27,
SIGWINCH: 28,
SIGIO: 29,
SIGPWR: 30,
SIGSYS: 31,
};
const sigStr = String(config.StopSignal).toUpperCase().replace(/^SIG/, '');
const sigNum = sigMap['SIG' + sigStr] ?? parseInt(config.StopSignal, 10);
@@ -213,9 +241,12 @@ const Container = Model.base.extend({
if (hostConfig.CapAdd && hostConfig.CapAdd.length) spec.cap_add = hostConfig.CapAdd;
if (hostConfig.CapDrop && hostConfig.CapDrop.length) spec.cap_drop = hostConfig.CapDrop;
if (hostConfig.Dns && hostConfig.Dns.length) spec.dns_server = hostConfig.Dns;
if (hostConfig.DnsOptions && hostConfig.DnsOptions.length) spec.dns_option = hostConfig.DnsOptions;
if (hostConfig.DnsSearch && hostConfig.DnsSearch.length) spec.dns_search = hostConfig.DnsSearch;
if (hostConfig.ExtraHosts && hostConfig.ExtraHosts.length) spec.hostadd = hostConfig.ExtraHosts;
if (hostConfig.DnsOptions && hostConfig.DnsOptions.length) spec.dns_option = hostConfig
.DnsOptions;
if (hostConfig.DnsSearch && hostConfig.DnsSearch.length) spec.dns_search = hostConfig
.DnsSearch;
if (hostConfig.ExtraHosts && hostConfig.ExtraHosts.length) spec.hostadd = hostConfig
.ExtraHosts;
if (hostConfig.GroupAdd && hostConfig.GroupAdd.length) spec.groups = hostConfig.GroupAdd;
if (hostConfig.ShmSize) spec.shm_size = hostConfig.ShmSize;
if (hostConfig.OomScoreAdj) spec.oom_score_adj = hostConfig.OomScoreAdj;
@@ -248,7 +279,10 @@ const Container = Model.base.extend({
const [port, protocol] = portProto.split('/');
const containerPort = parseInt(port, 10);
for (const b of bindings) {
const m = { container_port: containerPort, protocol: protocol || 'tcp' };
const m = {
container_port: containerPort,
protocol: protocol || 'tcp'
};
if (b.HostPort) m.host_port = parseInt(b.HostPort, 10);
if (b.HostIp && b.HostIp !== '0.0.0.0') m.host_ip = b.HostIp;
spec.portmappings.push(m);
@@ -260,9 +294,13 @@ const Container = Model.base.extend({
if (hostConfig.Memory > 0 || hostConfig.CpuQuota > 0) {
spec.resource_limits = {};
if (hostConfig.Memory > 0)
spec.resource_limits.memory = { limit: hostConfig.Memory };
spec.resource_limits.memory = {
limit: hostConfig.Memory
};
if (hostConfig.CpuQuota > 0) {
spec.resource_limits.cpu = { quota: hostConfig.CpuQuota };
spec.resource_limits.cpu = {
quota: hostConfig.CpuQuota
};
if (hostConfig.CpuPeriod > 0)
spec.resource_limits.cpu.period = hostConfig.CpuPeriod;
}
@@ -281,20 +319,28 @@ const Container = Model.base.extend({
}
if (bindMounts.length) {
spec.mounts = bindMounts.map(m => ({
Source: m.Source,
Destination: m.Destination,
ReadOnly: !m.RW,
options: m.Options || []
}));
spec.mounts = bindMounts.map(m => {
const options = (m.Options || []).slice();
if (!m.RW) options.push('ro');
return {
Type: 'bind',
Source: m.Source,
Destination: m.Destination,
Options: options
};
});
}
// Network
const nm = hostConfig.NetworkMode || '';
if (nm === 'host') {
spec.netns = { nsmode: 'host' };
spec.netns = {
nsmode: 'host'
};
} else if (nm === 'none') {
spec.netns = { nsmode: 'none' };
spec.netns = {
nsmode: 'none'
};
} else {
const nets = networkSettings.Networks || {};
const nonDefault = Object.keys(nets).filter(n => n !== 'bridge' && n !== 'default');
@@ -364,7 +410,13 @@ const Container = Model.base.extend({
const portBindings = networkPorts[port] || [];
if (portBindings.length === 0) {
ports.push({ string: port, port: portOnly, protocol: protocol || '', hostPort: null, hostIp: null });
ports.push({
string: port,
port: portOnly,
protocol: protocol || '',
hostPort: null,
hostIp: null
});
return;
}
@@ -393,16 +445,28 @@ const Container = Model.base.extend({
const cleanUrl = this.Image.split('@')[0];
const lastColon = cleanUrl.lastIndexOf(':');
const lastSlash = cleanUrl.lastIndexOf('/');
return { cleanUrl, lastColon, lastSlash };
return {
cleanUrl,
lastColon,
lastSlash
};
},
getImageRepo() {
const { cleanUrl, lastColon, lastSlash } = this._parseImageRef();
const {
cleanUrl,
lastColon,
lastSlash
} = this._parseImageRef();
return lastColon > lastSlash ? cleanUrl.substring(0, lastColon) : '';
},
getImageTag() {
const { cleanUrl, lastColon, lastSlash } = this._parseImageRef();
const {
cleanUrl,
lastColon,
lastSlash
} = this._parseImageRef();
return lastColon > lastSlash ? cleanUrl.substring(lastColon + 1) : 'latest';
},
@@ -446,15 +510,17 @@ const Container = Model.base.extend({
text = constants.ICON.INIT_DISABLED;
}
return E('span', { class: `autostart-status autostart-${status || ''}` }, text);
return E('span', {
class: `autostart-status autostart-${status || ''}`
}, text);
},
isRunning() {
const state = this.getState();
return this.State?.Running
|| state === 'running'
|| state === 'starting'
|| state === 'unhealthy';
return this.State?.Running ||
state === 'running' ||
state === 'starting' ||
state === 'unhealthy';
},
isStopped() {
@@ -530,7 +596,8 @@ const Container = Model.base.extend({
if (hasRestartPolicy && (initScriptStatus === 'none' || initScriptStatus === 'missing')) {
await this.generateInitScript();
await this.enableInitScript();
} else if (!hasRestartPolicy && (initScriptStatus === 'enabled' || initScriptStatus === 'disabled')) {
} else if (!hasRestartPolicy && (initScriptStatus === 'enabled' || initScriptStatus ===
'disabled')) {
await this.removeInitScript();
}
},
@@ -564,25 +631,39 @@ const Container = Model.base.extend({
},
async connect(networkName, payload) {
return Network.getSingleton({ name: networkName }).connect(Object.assign({ container: this.getID() }, payload));
return Network.getSingleton({
name: networkName
}).connect(Object.assign({
container: this.getID()
}, payload));
},
async disconnect(networkName) {
return Network.getSingleton({ name: networkName }).disconnect({ Container: this.getID() });
return Network.getSingleton({
name: networkName
}).disconnect({
Container: this.getID()
});
},
streamTop(onChunk, delay, psargs) {
const params = new URLSearchParams({ delay: String(delay || 2) });
const params = new URLSearchParams({
delay: String(delay || 2)
});
if (psargs) params.set('ps_args', psargs);
const url = L.url('admin/podman/stream/top', this.getID()) + '?' + params;
return this._stream(
() => url,
(data) => { if (!data.raw && !data.Error) onChunk(data); }
(data) => {
if (!data.raw && !data.Error) onChunk(data);
}
);
},
streamStats(onChunk, interval) {
const params = new URLSearchParams({ interval: String(interval || 2) });
const params = new URLSearchParams({
interval: String(interval || 2)
});
const url = L.url('admin/podman/stream/stats', this.getID()) + '?' + params;
return this._stream(
() => url,
@@ -595,7 +676,12 @@ const Container = Model.base.extend({
);
},
streamLogs(onChunk, { tail = 100, since = null, until = null, follow = true } = {}) {
streamLogs(onChunk, {
tail = 100,
since = null,
until = null,
follow = true
} = {}) {
if (!follow) {
// Historical fetch: single request, stop after completion (no reconnect)
let handle;
@@ -632,7 +718,9 @@ const Container = Model.base.extend({
return L.url('admin/podman/stream/logs', this.getID()) + '?' + params;
},
onChunk,
() => { reconnectSince = Date.now() / 1000; }
() => {
reconnectSince = Date.now() / 1000;
}
);
},
@@ -656,7 +744,9 @@ const Container = Model.base.extend({
const imageRef = image.getDisplayTag() || this.Image;
if (!imageRef || imageRef.startsWith('<none>'))
return { hasUpdate: false };
return {
hasUpdate: false
};
const localArch = image.Architecture || 'amd64';
const localOs = image.Os || 'linux';
@@ -666,7 +756,8 @@ const Container = Model.base.extend({
const remoteDigest = findArchDigest(manifest, localArch, localOs);
const repoDigests = image.RepoDigests || [];
const hasUpdate = !!(remoteDigest && !repoDigests.some(rd => rd.includes(remoteDigest)));
const hasUpdate = !!(remoteDigest && !repoDigests.some(rd => rd.includes(
remoteDigest)));
return {
hasUpdate,
@@ -727,37 +818,58 @@ const Container = Model.base.extend({
if (wasRunning) {
onProgress?.('→ ' + _('Stopping container') + '\n');
try { await this.stop(); }
catch (e) { throw new Error(_('Stop failed: %s').format(e.message || e)); }
try {
await this.stop();
} catch (e) {
throw new Error(_('Stop failed: %s').format(e.message || e));
}
}
onProgress?.('→ ' + _('Removing container') + '\n');
try { await this.remove(); }
catch (e) { throw new Error(_('Remove failed: %s').format(e.message || e)); }
try {
await this.remove();
} catch (e) {
throw new Error(_('Remove failed: %s').format(e.message || e));
}
onProgress?.('→ ' + _('Creating container') + '\n');
let created;
try { created = await ContainerRPC.create(spec); }
catch (e) { throw new Error(_('Create failed: %s').format(e.message || e)); }
try {
created = await ContainerRPC.create(spec);
} catch (e) {
throw new Error(_('Create failed: %s').format(e.message || e));
}
if (!created || created.error)
throw new Error(_('Create failed: %s').format(created?.error || _('Failed to create container')));
throw new Error(_('Create failed: %s').format(created?.error || _(
'Failed to create container')));
if (wasRunning) {
onProgress?.('→ ' + _('Starting container') + '\n');
try { await ContainerRPC.start(created.Id); }
catch (e) { throw new Error(_('Start failed: %s').format(e.message || e)); }
try {
await ContainerRPC.start(created.Id);
} catch (e) {
throw new Error(_('Start failed: %s').format(e.message || e));
}
}
onProgress?.('→ ' + _('Reinstating init script') + '\n');
try { await this._reinstateInitScript(initStatus); }
catch (e) { throw new Error(_('Init script reinstate failed: %s').format(e.message || e)); }
try {
await this._reinstateInitScript(initStatus);
} catch (e) {
throw new Error(_('Init script reinstate failed: %s').format(e.message || e));
}
onProgress?.('✓ ' + _('Updated to %s').format((newImageId || '').substring(0, 12)) + '\n');
return { oldImage: image };
onProgress?.('✓ ' + _('Updated to %s').format((newImageId || '').substring(0, 12)) +
'\n');
return {
oldImage: image
};
},
async getImage() {
const image = Image.getSingleton({ Id: this.ImageID || this.Image });
const image = Image.getSingleton({
Id: this.ImageID || this.Image
});
return Image.getSingleton(await image.inspect());
},