update duplicate
This commit is contained in:
@@ -2,7 +2,7 @@ import Hyperswarm from 'hyperswarm';
|
||||
import b4a from 'b4a';
|
||||
import { startTerminal, appendTerminalOutput } from './libs/terminal.js';
|
||||
import { startDockerTerminal, cleanUpDockerTerminal } from './libs/dockerTerminal.js';
|
||||
import { fetchTemplates, displayTemplateList, openDeployModal } from './libs/templateDeploy.js';
|
||||
import { fetchTemplates, displayTemplateList, openDeployModal, collectDuplicateFormData, populateDuplicateForm } from './libs/templateDeploy.js';
|
||||
import { showContainerSkeleton, createProgressBar, updateProgressBar, removeProgressBar, showOperationStatus } from './libs/loadingStates.js';
|
||||
import { closeAllModals, showStatusIndicator, hideStatusIndicator, showAlert } from './libs/uiUtils.js';
|
||||
|
||||
@@ -1396,17 +1396,37 @@ function openDuplicateModal(container) {
|
||||
|
||||
console.log(`[DEBUG] Retrieved container configuration: ${JSON.stringify(config)}`);
|
||||
|
||||
// Parse configuration and populate the modal fields
|
||||
// Parse configuration and populate the accordion form
|
||||
try {
|
||||
const CPUs = config.HostConfig?.CpusetCpus?.split(',') || [];
|
||||
// Clear the form first
|
||||
const form = document.getElementById('duplicate-container-form');
|
||||
if (form) form.reset();
|
||||
|
||||
document.getElementById('container-name').value = config.Name.replace(/^\//, '');
|
||||
document.getElementById('container-hostname').value = config.Config.Hostname || '';
|
||||
document.getElementById('container-image').value = config.Config.Image || '';
|
||||
document.getElementById('container-netmode').value = config.HostConfig?.NetworkMode || '';
|
||||
document.getElementById('container-cpu').value = CPUs.length || 0;
|
||||
document.getElementById('container-memory').value = Math.round(config.HostConfig?.Memory / (1024 * 1024)) || 0;
|
||||
document.getElementById('container-config').value = JSON.stringify(config, null, 2);
|
||||
// Populate all fields using the helper function
|
||||
populateDuplicateForm(config);
|
||||
|
||||
// Set up network mode change handler for duplicate modal
|
||||
const networkMode = document.getElementById('duplicate-network-mode');
|
||||
const customNetworkContainer = document.getElementById('duplicate-custom-network-container');
|
||||
if (networkMode && customNetworkContainer) {
|
||||
// Remove existing listeners by cloning and replacing
|
||||
const newNetworkMode = networkMode.cloneNode(true);
|
||||
networkMode.parentNode.replaceChild(newNetworkMode, networkMode);
|
||||
|
||||
newNetworkMode.addEventListener('change', (e) => {
|
||||
if (e.target.value === 'container') {
|
||||
customNetworkContainer.style.display = 'block';
|
||||
const input = customNetworkContainer.querySelector('input');
|
||||
if (input) input.placeholder = 'container-name';
|
||||
} else if (e.target.value !== 'host' && e.target.value !== 'none' && e.target.value !== 'bridge') {
|
||||
customNetworkContainer.style.display = 'block';
|
||||
const input = customNetworkContainer.querySelector('input');
|
||||
if (input) input.placeholder = 'network-name';
|
||||
} else {
|
||||
customNetworkContainer.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show the duplicate modal
|
||||
duplicateModal.show();
|
||||
@@ -1419,39 +1439,98 @@ function openDuplicateModal(container) {
|
||||
|
||||
|
||||
// Handle the Duplicate Container Form Submission
|
||||
duplicateContainerForm.addEventListener('submit', (e) => {
|
||||
duplicateContainerForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
duplicateModal.hide();
|
||||
|
||||
showStatusIndicator('Duplicating container...');
|
||||
|
||||
const name = document.getElementById('container-name').value.trim();
|
||||
const hostname = document.getElementById('container-hostname').value.trim();
|
||||
const image = document.getElementById('container-image').value.trim();
|
||||
const netmode = document.getElementById('container-netmode').value.trim();
|
||||
const cpu = document.getElementById('container-cpu').value.trim();
|
||||
const memory = document.getElementById('container-memory').value.trim();
|
||||
const configJSON = document.getElementById('container-config').value.trim();
|
||||
|
||||
let config;
|
||||
let formData;
|
||||
try {
|
||||
config = JSON.parse(configJSON);
|
||||
} catch (err) {
|
||||
hideStatusIndicator();
|
||||
showAlert('danger', 'Invalid JSON in configuration.');
|
||||
formData = collectDuplicateFormData();
|
||||
} catch (collectError) {
|
||||
console.error('[ERROR] Failed to collect duplicate form data:', collectError);
|
||||
showAlert('danger', 'Failed to collect form data. Check console for details.');
|
||||
return;
|
||||
}
|
||||
|
||||
sendCommand('duplicateContainer', { name, image, hostname, netmode, cpu, memory, config });
|
||||
// Validate required fields
|
||||
if (!formData.containerName || !formData.image) {
|
||||
showAlert('danger', 'Container name and image are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate delay for the demo
|
||||
setTimeout(() => {
|
||||
try {
|
||||
showStatusIndicator('Duplicating container...');
|
||||
|
||||
// Use deployContainer command with the collected form data
|
||||
// This reuses the same deployment logic
|
||||
const originalHandler = window.handlePeerResponse;
|
||||
let timeoutId = null;
|
||||
let isResolved = false;
|
||||
|
||||
const duplicateHandler = (response) => {
|
||||
if (isResolved) {
|
||||
if (typeof originalHandler === 'function') {
|
||||
originalHandler(response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isDuplicateResponse =
|
||||
(response.success && response.message && response.message.includes('deployed successfully')) ||
|
||||
(response.error && (response.message || response.error).includes('deploy') || response.error.includes('Container'));
|
||||
|
||||
if (isDuplicateResponse) {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
window.handlePeerResponse = originalHandler;
|
||||
isResolved = true;
|
||||
duplicateModal.hide();
|
||||
|
||||
if (response.success && response.message && response.message.includes('deployed successfully')) {
|
||||
hideStatusIndicator();
|
||||
showAlert('success', 'Container duplicated successfully!');
|
||||
|
||||
// Refresh container list
|
||||
showAlert('success', `Container "${formData.containerName}" duplicated successfully!`);
|
||||
sendCommand('listContainers');
|
||||
}, 2000); // Simulated processing time
|
||||
} else if (response.error) {
|
||||
hideStatusIndicator();
|
||||
const errorMessage = typeof response.error === 'string'
|
||||
? response.error
|
||||
: (response.error?.message || response.error?.toString() || 'Unknown error');
|
||||
showAlert('danger', errorMessage);
|
||||
}
|
||||
} else {
|
||||
if (typeof originalHandler === 'function') {
|
||||
originalHandler(response);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.handlePeerResponse = duplicateHandler;
|
||||
|
||||
if (typeof window.sendCommand === 'function') {
|
||||
window.sendCommand('deployContainer', formData);
|
||||
} else {
|
||||
window.handlePeerResponse = originalHandler;
|
||||
hideStatusIndicator();
|
||||
showAlert('danger', 'sendCommand is not available. Please ensure app.js is loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!isResolved) {
|
||||
window.handlePeerResponse = originalHandler;
|
||||
isResolved = true;
|
||||
hideStatusIndicator();
|
||||
duplicateModal.hide();
|
||||
showAlert('danger', 'Duplication timed out. No response from server.');
|
||||
}
|
||||
}, 60000);
|
||||
} catch (error) {
|
||||
hideStatusIndicator();
|
||||
console.error('[ERROR] Failed to duplicate container:', error);
|
||||
showAlert('danger', error.message || 'Failed to duplicate container. Check console for details.');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
+419
-18
@@ -708,43 +708,444 @@
|
||||
|
||||
<!-- Duplicate Container Modal -->
|
||||
<div class="modal fade" id="duplicateModal" tabindex="-1" aria-labelledby="duplicateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="duplicateModalLabel">Duplicate Container</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" style="max-height: 80vh; overflow-y: auto;">
|
||||
<form id="duplicate-container-form">
|
||||
<div class="mb-3">
|
||||
<label for="container-name" class="form-label">Container Name</label>
|
||||
<input type="text" class="form-control" id="container-name" required>
|
||||
<!-- Basic Settings (Always Visible) -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-primary mb-3"><i class="fas fa-cog"></i> Basic Settings</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="duplicate-container-name" class="form-label">Container Name <span class="text-danger">*</span></label>
|
||||
<input type="text" id="duplicate-container-name" class="form-control bg-dark text-white" placeholder="my-container" required>
|
||||
<small class="text-muted">Alphanumeric, dashes, and underscores only</small>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="duplicate-image" class="form-label">Image <span class="text-danger">*</span></label>
|
||||
<input type="text" id="duplicate-image" class="form-control bg-dark text-white" placeholder="nginx:latest" required>
|
||||
<small class="text-muted">Docker image name and tag</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="duplicate-command" class="form-label">Command Override</label>
|
||||
<input type="text" id="duplicate-command" class="form-control bg-dark text-white" placeholder="Override default command">
|
||||
<small class="text-muted">Override container CMD</small>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="duplicate-entrypoint" class="form-label">Entrypoint Override</label>
|
||||
<input type="text" id="duplicate-entrypoint" class="form-control bg-dark text-white" placeholder="Override default entrypoint">
|
||||
<small class="text-muted">Override container ENTRYPOINT</small>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="duplicate-workdir" class="form-label">Working Directory</label>
|
||||
<input type="text" id="duplicate-workdir" class="form-control bg-dark text-white" placeholder="/app">
|
||||
<small class="text-muted">Container working directory</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Accordion for Advanced Settings -->
|
||||
<div class="accordion accordion-flush" id="duplicateAccordion">
|
||||
<!-- Networking -->
|
||||
<div class="accordion-item bg-dark border-secondary">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed bg-dark text-white" type="button" data-bs-toggle="collapse" data-bs-target="#duplicateNetworkingCollapse">
|
||||
<i class="fas fa-network-wired me-2"></i> Networking
|
||||
</button>
|
||||
</h2>
|
||||
<div id="duplicateNetworkingCollapse" class="accordion-collapse collapse" data-bs-parent="#duplicateAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="duplicate-network-mode" class="form-label">Network Mode</label>
|
||||
<select id="duplicate-network-mode" class="form-select bg-dark text-white">
|
||||
<option value="bridge">Bridge</option>
|
||||
<option value="host">Host</option>
|
||||
<option value="none">None</option>
|
||||
<option value="container">Container</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6" id="duplicate-custom-network-container" style="display: none;">
|
||||
<label for="duplicate-custom-network" class="form-label">Custom Network</label>
|
||||
<input type="text" id="duplicate-custom-network" class="form-control bg-dark text-white" placeholder="network-name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="duplicate-hostname" class="form-label">Hostname</label>
|
||||
<input type="text" id="duplicate-hostname" class="form-control bg-dark text-white" placeholder="container-hostname">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="duplicate-domainname" class="form-label">Domain Name</label>
|
||||
<input type="text" id="duplicate-domainname" class="form-control bg-dark text-white" placeholder="example.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="container-hostname" class="form-label">Hostname</label>
|
||||
<input type="text" class="form-control" id="container-hostname" required>
|
||||
<label class="form-label">Port Mappings</label>
|
||||
<div id="duplicate-ports-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicatePortMapping()">
|
||||
<i class="fas fa-plus"></i> Add Port
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">Format: host:container/protocol (e.g., 8080:80/tcp)</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="container-image" class="form-label">Image</label>
|
||||
<input type="text" class="form-control" id="container-image" required>
|
||||
<label class="form-label">DNS Servers</label>
|
||||
<div id="duplicate-dns-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateDnsServer()">
|
||||
<i class="fas fa-plus"></i> Add DNS Server
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="container-netmode" class="form-label">Net Mode</label>
|
||||
<input type="text" class="form-control" id="container-netmode" required>
|
||||
<label class="form-label">Extra Hosts</label>
|
||||
<div id="duplicate-extra-hosts-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateExtraHost()">
|
||||
<i class="fas fa-plus"></i> Add Host
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">Format: hostname:ip (e.g., example.com:127.0.0.1)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Volumes & Storage -->
|
||||
<div class="accordion-item bg-dark border-secondary">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed bg-dark text-white" type="button" data-bs-toggle="collapse" data-bs-target="#duplicateVolumesCollapse">
|
||||
<i class="fas fa-hdd me-2"></i> Volumes & Storage
|
||||
</button>
|
||||
</h2>
|
||||
<div id="duplicateVolumesCollapse" class="accordion-collapse collapse" data-bs-parent="#duplicateAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Volume Mounts</label>
|
||||
<div id="duplicate-volumes-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateVolumeMount()">
|
||||
<i class="fas fa-plus"></i> Add Volume
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">Format: /host/path:/container/path:mode (mode: ro/rw)</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="container-cpu" class="form-label">CPU Count</label>
|
||||
<input type="number" class="form-control" id="container-cpu" required>
|
||||
<label class="form-label">Tmpfs Mounts</label>
|
||||
<div id="duplicate-tmpfs-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateTmpfsMount()">
|
||||
<i class="fas fa-plus"></i> Add Tmpfs
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">Format: /path:size (e.g., /tmp:100m)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resources -->
|
||||
<div class="accordion-item bg-dark border-secondary">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed bg-dark text-white" type="button" data-bs-toggle="collapse" data-bs-target="#duplicateResourcesCollapse">
|
||||
<i class="fas fa-server me-2"></i> Resources
|
||||
</button>
|
||||
</h2>
|
||||
<div id="duplicateResourcesCollapse" class="accordion-collapse collapse" data-bs-parent="#duplicateAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="duplicate-cpu-limit" class="form-label">CPU Limit (cores)</label>
|
||||
<input type="number" id="duplicate-cpu-limit" class="form-control bg-dark text-white" placeholder="2" step="0.1" min="0">
|
||||
<small class="text-muted">Number of CPU cores</small>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="duplicate-cpu-reservation" class="form-label">CPU Reservation</label>
|
||||
<input type="number" id="duplicate-cpu-reservation" class="form-control bg-dark text-white" placeholder="1" step="0.1" min="0">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="duplicate-cpu-shares" class="form-label">CPU Shares</label>
|
||||
<input type="number" id="duplicate-cpu-shares" class="form-control bg-dark text-white" placeholder="1024" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="duplicate-memory-limit" class="form-label">Memory Limit (MB)</label>
|
||||
<input type="number" id="duplicate-memory-limit" class="form-control bg-dark text-white" placeholder="512" min="0">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="duplicate-memory-reservation" class="form-label">Memory Reservation (MB)</label>
|
||||
<input type="number" id="duplicate-memory-reservation" class="form-control bg-dark text-white" placeholder="256" min="0">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="duplicate-memory-swap" class="form-label">Memory Swap (MB)</label>
|
||||
<input type="number" id="duplicate-memory-swap" class="form-control bg-dark text-white" placeholder="-1" min="-1">
|
||||
<small class="text-muted">-1 for unlimited</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="container-memory" class="form-label">Memory (MB)</label>
|
||||
<input type="number" class="form-control" id="container-memory" required>
|
||||
<label class="form-label">Device Mappings</label>
|
||||
<div id="duplicate-devices-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateDeviceMapping()">
|
||||
<i class="fas fa-plus"></i> Add Device
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">Format: /host/device:/container/device:permissions</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environment & Labels -->
|
||||
<div class="accordion-item bg-dark border-secondary">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed bg-dark text-white" type="button" data-bs-toggle="collapse" data-bs-target="#duplicateEnvLabelsCollapse">
|
||||
<i class="fas fa-tags me-2"></i> Environment & Labels
|
||||
</button>
|
||||
</h2>
|
||||
<div id="duplicateEnvLabelsCollapse" class="accordion-collapse collapse" data-bs-parent="#duplicateAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Environment Variables</label>
|
||||
<div id="duplicate-env"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateEnvVar()">
|
||||
<i class="fas fa-plus"></i> Add Variable
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="container-config" class="form-label">Container Configuration (JSON)</label>
|
||||
<textarea class="form-control" id="container-config" rows="10" required></textarea>
|
||||
<label class="form-label">Labels</label>
|
||||
<div id="duplicate-labels-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateLabel()">
|
||||
<i class="fas fa-plus"></i> Add Label
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">Format: key=value</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security -->
|
||||
<div class="accordion-item bg-dark border-secondary">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed bg-dark text-white" type="button" data-bs-toggle="collapse" data-bs-target="#duplicateSecurityCollapse">
|
||||
<i class="fas fa-shield-alt me-2"></i> Security
|
||||
</button>
|
||||
</h2>
|
||||
<div id="duplicateSecurityCollapse" class="accordion-collapse collapse" data-bs-parent="#duplicateAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="duplicate-user" class="form-label">User/UID</label>
|
||||
<input type="text" id="duplicate-user" class="form-control bg-dark text-white" placeholder="1000 or username">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="duplicate-group" class="form-label">Group/GID</label>
|
||||
<input type="text" id="duplicate-group" class="form-control bg-dark text-white" placeholder="1000 or groupname">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="duplicate-privileged">
|
||||
<label class="form-check-label" for="duplicate-privileged">Privileged Mode</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="duplicate-readonly-rootfs">
|
||||
<label class="form-check-label" for="duplicate-readonly-rootfs">Read-only Root Filesystem</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Capabilities</label>
|
||||
<div id="duplicate-capabilities-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateCapability()">
|
||||
<i class="fas fa-plus"></i> Add Capability
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">e.g., NET_ADMIN, SYS_ADMIN</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Security Options</label>
|
||||
<div id="duplicate-security-opts-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateSecurityOpt()">
|
||||
<i class="fas fa-plus"></i> Add Option
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">e.g., apparmor=profile, seccomp=unconfined</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Runtime & Behavior -->
|
||||
<div class="accordion-item bg-dark border-secondary">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed bg-dark text-white" type="button" data-bs-toggle="collapse" data-bs-target="#duplicateRuntimeCollapse">
|
||||
<i class="fas fa-play-circle me-2"></i> Runtime & Behavior
|
||||
</button>
|
||||
</h2>
|
||||
<div id="duplicateRuntimeCollapse" class="accordion-collapse collapse" data-bs-parent="#duplicateAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="duplicate-restart-policy" class="form-label">Restart Policy</label>
|
||||
<select id="duplicate-restart-policy" class="form-select bg-dark text-white">
|
||||
<option value="no">No</option>
|
||||
<option value="always">Always</option>
|
||||
<option value="on-failure">On Failure</option>
|
||||
<option value="unless-stopped">Unless Stopped</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="duplicate-restart-max-retries" class="form-label">Max Retries (on-failure)</label>
|
||||
<input type="number" id="duplicate-restart-max-retries" class="form-control bg-dark text-white" placeholder="10" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="duplicate-auto-remove">
|
||||
<label class="form-check-label" for="duplicate-auto-remove">Auto Remove</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="duplicate-tty">
|
||||
<label class="form-check-label" for="duplicate-tty">TTY</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="duplicate-stdin-open">
|
||||
<label class="form-check-label" for="duplicate-stdin-open">Interactive (Stdin Open)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="duplicate-detach">
|
||||
<label class="form-check-label" for="duplicate-detach">Detach Mode</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="duplicate-init">
|
||||
<label class="form-check-label" for="duplicate-init">Init Process</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Health & Logging -->
|
||||
<div class="accordion-item bg-dark border-secondary">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed bg-dark text-white" type="button" data-bs-toggle="collapse" data-bs-target="#duplicateHealthCollapse">
|
||||
<i class="fas fa-heartbeat me-2"></i> Health & Logging
|
||||
</button>
|
||||
</h2>
|
||||
<div id="duplicateHealthCollapse" class="accordion-collapse collapse" data-bs-parent="#duplicateAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="mb-3">
|
||||
<label for="duplicate-health-cmd" class="form-label">Health Check Command</label>
|
||||
<input type="text" id="duplicate-health-cmd" class="form-control bg-dark text-white" placeholder="CMD-SHELL curl -f http://localhost/ || exit 1">
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<label for="duplicate-health-interval" class="form-label">Interval (s)</label>
|
||||
<input type="number" id="duplicate-health-interval" class="form-control bg-dark text-white" placeholder="30" min="1">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="duplicate-health-timeout" class="form-label">Timeout (s)</label>
|
||||
<input type="number" id="duplicate-health-timeout" class="form-control bg-dark text-white" placeholder="10" min="1">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="duplicate-health-retries" class="form-label">Retries</label>
|
||||
<input type="number" id="duplicate-health-retries" class="form-control bg-dark text-white" placeholder="3" min="0">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="duplicate-health-start-period" class="form-label">Start Period (s)</label>
|
||||
<input type="number" id="duplicate-health-start-period" class="form-control bg-dark text-white" placeholder="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="duplicate-log-driver" class="form-label">Logging Driver</label>
|
||||
<select id="duplicate-log-driver" class="form-select bg-dark text-white">
|
||||
<option value="">Default</option>
|
||||
<option value="json-file">JSON File</option>
|
||||
<option value="syslog">Syslog</option>
|
||||
<option value="journald">Journald</option>
|
||||
<option value="gelf">GELF</option>
|
||||
<option value="fluentd">Fluentd</option>
|
||||
<option value="awslogs">AWS Logs</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Logging Options</label>
|
||||
<div id="duplicate-log-opts-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateLogOpt()">
|
||||
<i class="fas fa-plus"></i> Add Option
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced -->
|
||||
<div class="accordion-item bg-dark border-secondary">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed bg-dark text-white" type="button" data-bs-toggle="collapse" data-bs-target="#duplicateAdvancedCollapse">
|
||||
<i class="fas fa-cogs me-2"></i> Advanced
|
||||
</button>
|
||||
</h2>
|
||||
<div id="duplicateAdvancedCollapse" class="accordion-collapse collapse" data-bs-parent="#duplicateAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Sysctls</label>
|
||||
<div id="duplicate-sysctls-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateSysctl()">
|
||||
<i class="fas fa-plus"></i> Add Sysctl
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">Format: key=value (e.g., net.ipv4.ip_forward=1)</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Ulimits</label>
|
||||
<div id="duplicate-ulimits-container"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDuplicateUlimit()">
|
||||
<i class="fas fa-plus"></i> Add Ulimit
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">Format: name=soft:hard (e.g., nofile=1024:2048)</small>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="duplicate-oom-kill-disable">
|
||||
<label class="form-check-label" for="duplicate-oom-kill-disable">Disable OOM Killer</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="duplicate-pids-limit" class="form-label">PIDs Limit</label>
|
||||
<input type="number" id="duplicate-pids-limit" class="form-control bg-dark text-white" placeholder="-1" min="-1">
|
||||
<small class="text-muted">-1 for unlimited</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="duplicate-shm-size" class="form-label">Shared Memory Size (MB)</label>
|
||||
<input type="number" id="duplicate-shm-size" class="form-control bg-dark text-white" placeholder="64" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-clone"></i> Duplicate Container
|
||||
</button>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Deploy Duplicate</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+721
-1
@@ -831,5 +831,725 @@ deployForm.addEventListener('submit', async (e) => {
|
||||
// Initialize templates on load
|
||||
document.addEventListener('DOMContentLoaded', fetchTemplates);
|
||||
|
||||
// Duplicate modal array management functions
|
||||
let duplicatePortCounter = 0;
|
||||
let duplicateVolumeCounter = 0;
|
||||
let duplicateEnvCounter = 0;
|
||||
let duplicateLabelCounter = 0;
|
||||
let duplicateDnsCounter = 0;
|
||||
let duplicateExtraHostCounter = 0;
|
||||
let duplicateDeviceCounter = 0;
|
||||
let duplicateCapabilityCounter = 0;
|
||||
let duplicateSecurityOptCounter = 0;
|
||||
let duplicateLogOptCounter = 0;
|
||||
let duplicateSysctlCounter = 0;
|
||||
let duplicateUlimitCounter = 0;
|
||||
let duplicateTmpfsCounter = 0;
|
||||
|
||||
function addDuplicatePortMapping() {
|
||||
const container = document.getElementById('duplicate-ports-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-port-${duplicatePortCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="8080:80/tcp" data-port-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateVolumeMount() {
|
||||
const container = document.getElementById('duplicate-volumes-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-volume-${duplicateVolumeCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="/host/path:/container/path:ro" data-volume-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateTmpfsMount() {
|
||||
const container = document.getElementById('duplicate-tmpfs-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-tmpfs-${duplicateTmpfsCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="/tmp:100m" data-tmpfs-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateEnvVar() {
|
||||
const container = document.getElementById('duplicate-env');
|
||||
if (!container) return;
|
||||
const id = `duplicate-env-${duplicateEnvCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item mb-2';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="KEY" data-env-key="${id}" style="flex: 0 0 40%;">
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="value" data-env-value="${id}" style="flex: 1;">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateLabel() {
|
||||
const container = document.getElementById('duplicate-labels-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-label-${duplicateLabelCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="key=value" data-label-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateDnsServer() {
|
||||
const container = document.getElementById('duplicate-dns-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-dns-${duplicateDnsCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="8.8.8.8" data-dns-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateExtraHost() {
|
||||
const container = document.getElementById('duplicate-extra-hosts-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-host-${duplicateExtraHostCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="example.com:127.0.0.1" data-host-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateDeviceMapping() {
|
||||
const container = document.getElementById('duplicate-devices-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-device-${duplicateDeviceCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="/dev/ttyUSB0:/dev/ttyUSB0:rwm" data-device-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateCapability() {
|
||||
const container = document.getElementById('duplicate-capabilities-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-cap-${duplicateCapabilityCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="NET_ADMIN" data-cap-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateSecurityOpt() {
|
||||
const container = document.getElementById('duplicate-security-opts-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-secopt-${duplicateSecurityOptCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="apparmor=profile" data-secopt-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateLogOpt() {
|
||||
const container = document.getElementById('duplicate-log-opts-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-logopt-${duplicateLogOptCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="max-size=10m" data-logopt-key="${id}" style="flex: 0 0 40%;">
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="value" data-logopt-value="${id}" style="flex: 1;">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateSysctl() {
|
||||
const container = document.getElementById('duplicate-sysctls-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-sysctl-${duplicateSysctlCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="net.ipv4.ip_forward=1" data-sysctl-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
function addDuplicateUlimit() {
|
||||
const container = document.getElementById('duplicate-ulimits-container');
|
||||
if (!container) return;
|
||||
const id = `duplicate-ulimit-${duplicateUlimitCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
item.id = id;
|
||||
item.innerHTML = `
|
||||
<input type="text" class="form-control bg-dark text-white" placeholder="nofile=1024:2048" data-ulimit-id="${id}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeArrayItem('${id}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
// Make duplicate functions globally available
|
||||
window.addDuplicatePortMapping = addDuplicatePortMapping;
|
||||
window.addDuplicateVolumeMount = addDuplicateVolumeMount;
|
||||
window.addDuplicateTmpfsMount = addDuplicateTmpfsMount;
|
||||
window.addDuplicateEnvVar = addDuplicateEnvVar;
|
||||
window.addDuplicateLabel = addDuplicateLabel;
|
||||
window.addDuplicateDnsServer = addDuplicateDnsServer;
|
||||
window.addDuplicateExtraHost = addDuplicateExtraHost;
|
||||
window.addDuplicateDeviceMapping = addDuplicateDeviceMapping;
|
||||
window.addDuplicateCapability = addDuplicateCapability;
|
||||
window.addDuplicateSecurityOpt = addDuplicateSecurityOpt;
|
||||
window.addDuplicateLogOpt = addDuplicateLogOpt;
|
||||
window.addDuplicateSysctl = addDuplicateSysctl;
|
||||
window.addDuplicateUlimit = addDuplicateUlimit;
|
||||
|
||||
// Collect duplicate form data (similar to collectFormData but for duplicate modal)
|
||||
function collectDuplicateFormData() {
|
||||
const data = {
|
||||
containerName: document.getElementById('duplicate-container-name')?.value.trim() || '',
|
||||
image: document.getElementById('duplicate-image')?.value.trim() || '',
|
||||
command: document.getElementById('duplicate-command')?.value.trim() || null,
|
||||
entrypoint: document.getElementById('duplicate-entrypoint')?.value.trim() || null,
|
||||
workingDir: document.getElementById('duplicate-workdir')?.value.trim() || null,
|
||||
|
||||
// Networking
|
||||
networkMode: document.getElementById('duplicate-network-mode')?.value || 'bridge',
|
||||
customNetwork: (() => {
|
||||
const mode = document.getElementById('duplicate-network-mode')?.value;
|
||||
const customNet = document.getElementById('duplicate-custom-network')?.value.trim();
|
||||
if ((mode === 'container' || (mode && mode !== 'bridge' && mode !== 'host' && mode !== 'none')) && customNet) {
|
||||
return customNet;
|
||||
}
|
||||
return null;
|
||||
})(),
|
||||
hostname: document.getElementById('duplicate-hostname')?.value.trim() || null,
|
||||
domainname: document.getElementById('duplicate-domainname')?.value.trim() || null,
|
||||
ports: collectArrayItems('duplicate-ports-container', 'data-port-id'),
|
||||
dns: collectArrayItems('duplicate-dns-container', 'data-dns-id'),
|
||||
extraHosts: collectArrayItems('duplicate-extra-hosts-container', 'data-host-id'),
|
||||
|
||||
// Volumes
|
||||
volumes: collectArrayItems('duplicate-volumes-container', 'data-volume-id'),
|
||||
tmpfs: collectArrayItems('duplicate-tmpfs-container', 'data-tmpfs-id'),
|
||||
|
||||
// Resources
|
||||
cpuLimit: parseFloat(document.getElementById('duplicate-cpu-limit')?.value) || null,
|
||||
cpuReservation: parseFloat(document.getElementById('duplicate-cpu-reservation')?.value) || null,
|
||||
cpuShares: parseInt(document.getElementById('duplicate-cpu-shares')?.value) || null,
|
||||
memoryLimit: parseInt(document.getElementById('duplicate-memory-limit')?.value) || null,
|
||||
memoryReservation: parseInt(document.getElementById('duplicate-memory-reservation')?.value) || null,
|
||||
memorySwap: parseInt(document.getElementById('duplicate-memory-swap')?.value) || null,
|
||||
devices: collectArrayItems('duplicate-devices-container', 'data-device-id'),
|
||||
|
||||
// Environment & Labels
|
||||
env: collectDuplicateEnvVars(),
|
||||
labels: collectDuplicateLabels(),
|
||||
|
||||
// Security
|
||||
user: document.getElementById('duplicate-user')?.value.trim() || null,
|
||||
group: document.getElementById('duplicate-group')?.value.trim() || null,
|
||||
privileged: document.getElementById('duplicate-privileged')?.checked || false,
|
||||
readonlyRootfs: document.getElementById('duplicate-readonly-rootfs')?.checked || false,
|
||||
capabilities: collectArrayItems('duplicate-capabilities-container', 'data-cap-id'),
|
||||
securityOpts: collectArrayItems('duplicate-security-opts-container', 'data-secopt-id'),
|
||||
|
||||
// Runtime
|
||||
restartPolicy: document.getElementById('duplicate-restart-policy')?.value || 'no',
|
||||
restartMaxRetries: parseInt(document.getElementById('duplicate-restart-max-retries')?.value) || null,
|
||||
autoRemove: document.getElementById('duplicate-auto-remove')?.checked || false,
|
||||
tty: document.getElementById('duplicate-tty')?.checked || false,
|
||||
stdinOpen: document.getElementById('duplicate-stdin-open')?.checked || false,
|
||||
detach: document.getElementById('duplicate-detach')?.checked !== false,
|
||||
init: document.getElementById('duplicate-init')?.checked || false,
|
||||
|
||||
// Health & Logging
|
||||
healthCmd: document.getElementById('duplicate-health-cmd')?.value.trim() || null,
|
||||
healthInterval: parseInt(document.getElementById('duplicate-health-interval')?.value) || null,
|
||||
healthTimeout: parseInt(document.getElementById('duplicate-health-timeout')?.value) || null,
|
||||
healthRetries: parseInt(document.getElementById('duplicate-health-retries')?.value) || null,
|
||||
healthStartPeriod: parseInt(document.getElementById('duplicate-health-start-period')?.value) || null,
|
||||
logDriver: document.getElementById('duplicate-log-driver')?.value || null,
|
||||
logOpts: collectDuplicateLogOpts(),
|
||||
|
||||
// Advanced
|
||||
sysctls: collectDuplicateSysctls(),
|
||||
ulimits: collectDuplicateUlimits(),
|
||||
oomKillDisable: document.getElementById('duplicate-oom-kill-disable')?.checked || false,
|
||||
pidsLimit: parseInt(document.getElementById('duplicate-pids-limit')?.value) || null,
|
||||
shmSize: parseInt(document.getElementById('duplicate-shm-size')?.value) || null,
|
||||
};
|
||||
|
||||
// Remove null/empty values
|
||||
Object.keys(data).forEach(key => {
|
||||
if (data[key] === null || data[key] === '' || (Array.isArray(data[key]) && data[key].length === 0)) {
|
||||
delete data[key];
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function collectDuplicateEnvVars() {
|
||||
const container = document.getElementById('duplicate-env');
|
||||
if (!container) return [];
|
||||
const envVars = [];
|
||||
container.querySelectorAll('[data-env-key]').forEach(keyInput => {
|
||||
const key = keyInput.value.trim();
|
||||
const valueInput = container.querySelector(`[data-env-value="${keyInput.getAttribute('data-env-key')}"]`);
|
||||
const value = valueInput?.value.trim() || '';
|
||||
if (key) {
|
||||
envVars.push({ name: key, value });
|
||||
}
|
||||
});
|
||||
return envVars;
|
||||
}
|
||||
|
||||
function collectDuplicateLabels() {
|
||||
const items = collectArrayItems('duplicate-labels-container', 'data-label-id');
|
||||
const labels = {};
|
||||
items.forEach(item => {
|
||||
const [key, value] = item.split('=');
|
||||
if (key && value) {
|
||||
labels[key.trim()] = value.trim();
|
||||
}
|
||||
});
|
||||
return Object.keys(labels).length > 0 ? labels : null;
|
||||
}
|
||||
|
||||
function collectDuplicateLogOpts() {
|
||||
const container = document.getElementById('duplicate-log-opts-container');
|
||||
if (!container) return null;
|
||||
const opts = {};
|
||||
container.querySelectorAll('[data-logopt-key]').forEach(keyInput => {
|
||||
const key = keyInput.value.trim();
|
||||
const valueInput = container.querySelector(`[data-logopt-value="${keyInput.getAttribute('data-logopt-key')}"]`);
|
||||
const value = valueInput?.value.trim() || '';
|
||||
if (key) {
|
||||
opts[key] = value;
|
||||
}
|
||||
});
|
||||
return Object.keys(opts).length > 0 ? opts : null;
|
||||
}
|
||||
|
||||
function collectDuplicateSysctls() {
|
||||
const items = collectArrayItems('duplicate-sysctls-container', 'data-sysctl-id');
|
||||
const sysctls = {};
|
||||
items.forEach(item => {
|
||||
const [key, value] = item.split('=');
|
||||
if (key && value) {
|
||||
sysctls[key.trim()] = value.trim();
|
||||
}
|
||||
});
|
||||
return Object.keys(sysctls).length > 0 ? sysctls : null;
|
||||
}
|
||||
|
||||
function collectDuplicateUlimits() {
|
||||
const items = collectArrayItems('duplicate-ulimits-container', 'data-ulimit-id');
|
||||
const ulimits = [];
|
||||
items.forEach(item => {
|
||||
const [name, limits] = item.split('=');
|
||||
if (name && limits) {
|
||||
const [soft, hard] = limits.split(':');
|
||||
ulimits.push({
|
||||
Name: name.trim(),
|
||||
Soft: soft ? parseInt(soft) : null,
|
||||
Hard: hard ? parseInt(hard) : null
|
||||
});
|
||||
}
|
||||
});
|
||||
return ulimits.length > 0 ? ulimits : null;
|
||||
}
|
||||
|
||||
// Populate duplicate form from container config
|
||||
function populateDuplicateForm(config) {
|
||||
if (!config) return;
|
||||
|
||||
// Reset counters
|
||||
duplicatePortCounter = duplicateVolumeCounter = duplicateEnvCounter = duplicateLabelCounter = duplicateDnsCounter = 0;
|
||||
duplicateExtraHostCounter = duplicateDeviceCounter = duplicateCapabilityCounter = duplicateSecurityOptCounter = 0;
|
||||
duplicateLogOptCounter = duplicateSysctlCounter = duplicateUlimitCounter = duplicateTmpfsCounter = 0;
|
||||
|
||||
// Clear all array containers
|
||||
['duplicate-ports-container', 'duplicate-volumes-container', 'duplicate-env', 'duplicate-labels-container',
|
||||
'duplicate-dns-container', 'duplicate-extra-hosts-container', 'duplicate-devices-container',
|
||||
'duplicate-capabilities-container', 'duplicate-security-opts-container', 'duplicate-log-opts-container',
|
||||
'duplicate-sysctls-container', 'duplicate-ulimits-container', 'duplicate-tmpfs-container'].forEach(id => {
|
||||
const container = document.getElementById(id);
|
||||
if (container) container.innerHTML = '';
|
||||
});
|
||||
|
||||
// Basic settings
|
||||
const nameEl = document.getElementById('duplicate-container-name');
|
||||
if (nameEl) nameEl.value = config.Name ? config.Name.replace(/^\//, '') : '';
|
||||
|
||||
const imageEl = document.getElementById('duplicate-image');
|
||||
if (imageEl) imageEl.value = config.Config?.Image || '';
|
||||
|
||||
const commandEl = document.getElementById('duplicate-command');
|
||||
if (commandEl && config.Config?.Cmd) {
|
||||
commandEl.value = Array.isArray(config.Config.Cmd) ? config.Config.Cmd.join(' ') : config.Config.Cmd;
|
||||
}
|
||||
|
||||
const entrypointEl = document.getElementById('duplicate-entrypoint');
|
||||
if (entrypointEl && config.Config?.Entrypoint) {
|
||||
entrypointEl.value = Array.isArray(config.Config.Entrypoint) ? config.Config.Entrypoint.join(' ') : config.Config.Entrypoint;
|
||||
}
|
||||
|
||||
const workdirEl = document.getElementById('duplicate-workdir');
|
||||
if (workdirEl) workdirEl.value = config.Config?.WorkingDir || '';
|
||||
|
||||
// Networking
|
||||
const networkModeEl = document.getElementById('duplicate-network-mode');
|
||||
if (networkModeEl && config.HostConfig?.NetworkMode) {
|
||||
const netMode = config.HostConfig.NetworkMode;
|
||||
if (netMode.startsWith('container:')) {
|
||||
networkModeEl.value = 'container';
|
||||
const customNetEl = document.getElementById('duplicate-custom-network');
|
||||
if (customNetEl) {
|
||||
customNetEl.value = netMode.replace('container:', '');
|
||||
document.getElementById('duplicate-custom-network-container').style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
networkModeEl.value = netMode;
|
||||
}
|
||||
}
|
||||
|
||||
const hostnameEl = document.getElementById('duplicate-hostname');
|
||||
if (hostnameEl) hostnameEl.value = config.Config?.Hostname || '';
|
||||
|
||||
const domainnameEl = document.getElementById('duplicate-domainname');
|
||||
if (domainnameEl) domainnameEl.value = config.Config?.Domainname || '';
|
||||
|
||||
// Port bindings
|
||||
if (config.HostConfig?.PortBindings) {
|
||||
Object.entries(config.HostConfig.PortBindings).forEach(([containerPort, bindings]) => {
|
||||
if (bindings && bindings.length > 0) {
|
||||
const binding = bindings[0];
|
||||
const portStr = binding.HostPort ? `${binding.HostPort}:${containerPort}` : containerPort;
|
||||
addDuplicatePortMapping();
|
||||
const container = document.getElementById('duplicate-ports-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = portStr;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// DNS
|
||||
if (config.HostConfig?.Dns && Array.isArray(config.HostConfig.Dns)) {
|
||||
config.HostConfig.Dns.forEach(dns => {
|
||||
addDuplicateDnsServer();
|
||||
const container = document.getElementById('duplicate-dns-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = dns;
|
||||
});
|
||||
}
|
||||
|
||||
// Extra hosts
|
||||
if (config.HostConfig?.ExtraHosts && Array.isArray(config.HostConfig.ExtraHosts)) {
|
||||
config.HostConfig.ExtraHosts.forEach(host => {
|
||||
addDuplicateExtraHost();
|
||||
const container = document.getElementById('duplicate-extra-hosts-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = host;
|
||||
});
|
||||
}
|
||||
|
||||
// Volumes
|
||||
if (config.HostConfig?.Binds && Array.isArray(config.HostConfig.Binds)) {
|
||||
config.HostConfig.Binds.forEach(bind => {
|
||||
addDuplicateVolumeMount();
|
||||
const container = document.getElementById('duplicate-volumes-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = bind;
|
||||
});
|
||||
}
|
||||
|
||||
// Tmpfs
|
||||
if (config.HostConfig?.Tmpfs && typeof config.HostConfig.Tmpfs === 'object') {
|
||||
Object.entries(config.HostConfig.Tmpfs).forEach(([path, opts]) => {
|
||||
addDuplicateTmpfsMount();
|
||||
const container = document.getElementById('duplicate-tmpfs-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = opts ? `${path}:${opts}` : path;
|
||||
});
|
||||
}
|
||||
|
||||
// Resources
|
||||
if (config.HostConfig?.NanoCpus) {
|
||||
const cpuLimitEl = document.getElementById('duplicate-cpu-limit');
|
||||
if (cpuLimitEl) cpuLimitEl.value = config.HostConfig.NanoCpus / 1000000000;
|
||||
}
|
||||
if (config.HostConfig?.CpuQuota) {
|
||||
const cpuReservationEl = document.getElementById('duplicate-cpu-reservation');
|
||||
if (cpuReservationEl) cpuReservationEl.value = config.HostConfig.CpuQuota / 1000000000;
|
||||
}
|
||||
if (config.HostConfig?.CpuShares) {
|
||||
const cpuSharesEl = document.getElementById('duplicate-cpu-shares');
|
||||
if (cpuSharesEl) cpuSharesEl.value = config.HostConfig.CpuShares;
|
||||
}
|
||||
if (config.HostConfig?.Memory) {
|
||||
const memoryLimitEl = document.getElementById('duplicate-memory-limit');
|
||||
if (memoryLimitEl) memoryLimitEl.value = Math.round(config.HostConfig.Memory / (1024 * 1024));
|
||||
}
|
||||
if (config.HostConfig?.MemoryReservation) {
|
||||
const memoryReservationEl = document.getElementById('duplicate-memory-reservation');
|
||||
if (memoryReservationEl) memoryReservationEl.value = Math.round(config.HostConfig.MemoryReservation / (1024 * 1024));
|
||||
}
|
||||
if (config.HostConfig?.MemorySwap !== undefined) {
|
||||
const memorySwapEl = document.getElementById('duplicate-memory-swap');
|
||||
if (memorySwapEl) memorySwapEl.value = config.HostConfig.MemorySwap === -1 ? -1 : Math.round(config.HostConfig.MemorySwap / (1024 * 1024));
|
||||
}
|
||||
|
||||
// Devices
|
||||
if (config.HostConfig?.Devices && Array.isArray(config.HostConfig.Devices)) {
|
||||
config.HostConfig.Devices.forEach(device => {
|
||||
const deviceStr = `${device.PathOnHost}:${device.PathInContainer}:${device.CgroupPermissions || 'rwm'}`;
|
||||
addDuplicateDeviceMapping();
|
||||
const container = document.getElementById('duplicate-devices-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = deviceStr;
|
||||
});
|
||||
}
|
||||
|
||||
// Environment variables
|
||||
if (config.Config?.Env && Array.isArray(config.Config.Env)) {
|
||||
config.Config.Env.forEach(envStr => {
|
||||
const [name, ...valueParts] = envStr.split('=');
|
||||
const value = valueParts.join('=');
|
||||
addDuplicateEnvVar();
|
||||
const container = document.getElementById('duplicate-env');
|
||||
const items = container.querySelectorAll('.array-item');
|
||||
const lastItem = items[items.length - 1];
|
||||
if (lastItem) {
|
||||
const keyInput = lastItem.querySelector('[data-env-key]');
|
||||
const valueInput = lastItem.querySelector('[data-env-value]');
|
||||
if (keyInput) keyInput.value = name || '';
|
||||
if (valueInput) valueInput.value = value || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Labels
|
||||
if (config.Config?.Labels && typeof config.Config.Labels === 'object') {
|
||||
Object.entries(config.Config.Labels).forEach(([key, value]) => {
|
||||
addDuplicateLabel();
|
||||
const container = document.getElementById('duplicate-labels-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = `${key}=${value}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Security
|
||||
const userEl = document.getElementById('duplicate-user');
|
||||
if (userEl) userEl.value = config.Config?.User || '';
|
||||
|
||||
const groupEl = document.getElementById('duplicate-group');
|
||||
if (groupEl && config.HostConfig?.GroupAdd && config.HostConfig.GroupAdd.length > 0) {
|
||||
groupEl.value = config.HostConfig.GroupAdd[0];
|
||||
}
|
||||
|
||||
const privilegedEl = document.getElementById('duplicate-privileged');
|
||||
if (privilegedEl) privilegedEl.checked = config.HostConfig?.Privileged || false;
|
||||
|
||||
const readonlyRootfsEl = document.getElementById('duplicate-readonly-rootfs');
|
||||
if (readonlyRootfsEl) readonlyRootfsEl.checked = config.HostConfig?.ReadonlyRootfs || false;
|
||||
|
||||
// Capabilities
|
||||
if (config.HostConfig?.CapAdd && Array.isArray(config.HostConfig.CapAdd)) {
|
||||
config.HostConfig.CapAdd.forEach(cap => {
|
||||
addDuplicateCapability();
|
||||
const container = document.getElementById('duplicate-capabilities-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = cap;
|
||||
});
|
||||
}
|
||||
|
||||
// Security options
|
||||
if (config.HostConfig?.SecurityOpt && Array.isArray(config.HostConfig.SecurityOpt)) {
|
||||
config.HostConfig.SecurityOpt.forEach(opt => {
|
||||
addDuplicateSecurityOpt();
|
||||
const container = document.getElementById('duplicate-security-opts-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = opt;
|
||||
});
|
||||
}
|
||||
|
||||
// Runtime
|
||||
const restartPolicyEl = document.getElementById('duplicate-restart-policy');
|
||||
if (restartPolicyEl && config.HostConfig?.RestartPolicy) {
|
||||
restartPolicyEl.value = config.HostConfig.RestartPolicy.Name || 'no';
|
||||
const restartMaxRetriesEl = document.getElementById('duplicate-restart-max-retries');
|
||||
if (restartMaxRetriesEl && config.HostConfig.RestartPolicy.MaximumRetryCount) {
|
||||
restartMaxRetriesEl.value = config.HostConfig.RestartPolicy.MaximumRetryCount;
|
||||
}
|
||||
}
|
||||
|
||||
const autoRemoveEl = document.getElementById('duplicate-auto-remove');
|
||||
if (autoRemoveEl) autoRemoveEl.checked = config.HostConfig?.AutoRemove || false;
|
||||
|
||||
const ttyEl = document.getElementById('duplicate-tty');
|
||||
if (ttyEl) ttyEl.checked = config.Config?.Tty || false;
|
||||
|
||||
const stdinOpenEl = document.getElementById('duplicate-stdin-open');
|
||||
if (stdinOpenEl) stdinOpenEl.checked = config.Config?.OpenStdin || false;
|
||||
|
||||
const detachEl = document.getElementById('duplicate-detach');
|
||||
if (detachEl) detachEl.checked = config.Config?.AttachStdin === false;
|
||||
|
||||
const initEl = document.getElementById('duplicate-init');
|
||||
if (initEl) initEl.checked = config.HostConfig?.Init || false;
|
||||
|
||||
// Health check
|
||||
if (config.Config?.Healthcheck) {
|
||||
const healthCmdEl = document.getElementById('duplicate-health-cmd');
|
||||
if (healthCmdEl && config.Config.Healthcheck.Test) {
|
||||
const test = config.Config.Healthcheck.Test;
|
||||
if (Array.isArray(test)) {
|
||||
healthCmdEl.value = test.join(' ');
|
||||
} else {
|
||||
healthCmdEl.value = test;
|
||||
}
|
||||
}
|
||||
const healthIntervalEl = document.getElementById('duplicate-health-interval');
|
||||
if (healthIntervalEl && config.Config.Healthcheck.Interval) {
|
||||
healthIntervalEl.value = Math.round(config.Config.Healthcheck.Interval / 1000000000);
|
||||
}
|
||||
const healthTimeoutEl = document.getElementById('duplicate-health-timeout');
|
||||
if (healthTimeoutEl && config.Config.Healthcheck.Timeout) {
|
||||
healthTimeoutEl.value = Math.round(config.Config.Healthcheck.Timeout / 1000000000);
|
||||
}
|
||||
const healthRetriesEl = document.getElementById('duplicate-health-retries');
|
||||
if (healthRetriesEl && config.Config.Healthcheck.Retries) {
|
||||
healthRetriesEl.value = config.Config.Healthcheck.Retries;
|
||||
}
|
||||
const healthStartPeriodEl = document.getElementById('duplicate-health-start-period');
|
||||
if (healthStartPeriodEl && config.Config.Healthcheck.StartPeriod) {
|
||||
healthStartPeriodEl.value = Math.round(config.Config.Healthcheck.StartPeriod / 1000000000);
|
||||
}
|
||||
}
|
||||
|
||||
// Logging
|
||||
if (config.HostConfig?.LogConfig) {
|
||||
const logDriverEl = document.getElementById('duplicate-log-driver');
|
||||
if (logDriverEl) logDriverEl.value = config.HostConfig.LogConfig.Type || '';
|
||||
|
||||
if (config.HostConfig.LogConfig.Config && typeof config.HostConfig.LogConfig.Config === 'object') {
|
||||
Object.entries(config.HostConfig.LogConfig.Config).forEach(([key, value]) => {
|
||||
addDuplicateLogOpt();
|
||||
const container = document.getElementById('duplicate-log-opts-container');
|
||||
const items = container.querySelectorAll('.array-item');
|
||||
const lastItem = items[items.length - 1];
|
||||
if (lastItem) {
|
||||
const keyInput = lastItem.querySelector('[data-logopt-key]');
|
||||
const valueInput = lastItem.querySelector('[data-logopt-value]');
|
||||
if (keyInput) keyInput.value = key;
|
||||
if (valueInput) valueInput.value = value || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Advanced
|
||||
if (config.HostConfig?.Sysctls && typeof config.HostConfig.Sysctls === 'object') {
|
||||
Object.entries(config.HostConfig.Sysctls).forEach(([key, value]) => {
|
||||
addDuplicateSysctl();
|
||||
const container = document.getElementById('duplicate-sysctls-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = `${key}=${value}`;
|
||||
});
|
||||
}
|
||||
|
||||
if (config.HostConfig?.Ulimits && Array.isArray(config.HostConfig.Ulimits)) {
|
||||
config.HostConfig.Ulimits.forEach(ulimit => {
|
||||
const ulimitStr = `${ulimit.Name}=${ulimit.Soft || ''}:${ulimit.Hard || ''}`;
|
||||
addDuplicateUlimit();
|
||||
const container = document.getElementById('duplicate-ulimits-container');
|
||||
const lastInput = container?.lastElementChild?.querySelector('input');
|
||||
if (lastInput) lastInput.value = ulimitStr;
|
||||
});
|
||||
}
|
||||
|
||||
const oomKillDisableEl = document.getElementById('duplicate-oom-kill-disable');
|
||||
if (oomKillDisableEl) oomKillDisableEl.checked = config.HostConfig?.OomKillDisable || false;
|
||||
|
||||
const pidsLimitEl = document.getElementById('duplicate-pids-limit');
|
||||
if (pidsLimitEl && config.HostConfig?.PidsLimit !== undefined) {
|
||||
pidsLimitEl.value = config.HostConfig.PidsLimit === 0 ? -1 : config.HostConfig.PidsLimit;
|
||||
}
|
||||
|
||||
const shmSizeEl = document.getElementById('duplicate-shm-size');
|
||||
if (shmSizeEl && config.HostConfig?.ShmSize) {
|
||||
shmSizeEl.value = Math.round(config.HostConfig.ShmSize / (1024 * 1024));
|
||||
}
|
||||
}
|
||||
|
||||
// Export required functions
|
||||
export { fetchTemplates, displayTemplateList, openDeployModal };
|
||||
export { fetchTemplates, displayTemplateList, openDeployModal, collectDuplicateFormData, populateDuplicateForm };
|
||||
|
||||
Reference in New Issue
Block a user