updating deployer

This commit is contained in:
Raven Scott
2025-11-24 07:26:12 -05:00
parent d3216afb64
commit 2062909878
3 changed files with 1338 additions and 180 deletions
+486 -19
View File
@@ -372,6 +372,53 @@
/* Even lighter color when active */
}
/* Accordion styling for deploy modal */
.accordion-item {
background-color: #2c2c2c;
border-color: #444;
}
.accordion-button {
background-color: #2c2c2c;
color: #fff;
}
.accordion-button:not(.collapsed) {
background-color: #3c3c3c;
color: #fff;
}
.accordion-button:focus {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.accordion-body {
background-color: #1a1a1a;
}
/* Form control styling in dark mode */
.form-control:focus,
.form-select:focus {
background-color: #2c2c2c;
border-color: #0d6efd;
color: #fff;
}
/* Array item styling */
.array-item {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
}
.array-item input {
flex: 1;
}
.array-item .btn-sm {
flex-shrink: 0;
}
.list-group-item {
position: relative;
@@ -603,35 +650,455 @@
<!-- Deploy Template Modal -->
<div class="modal fade" id="templateDeployModalUnique" tabindex="-1" aria-labelledby="templateDeployModalLabel" 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="deploy-title">Deploy Template</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="deploy-form">
<div class="form-group mb-3">
<label for="deploy-container-name">Container Name</label>
<input type="text" id="deploy-container-name" class="form-control" placeholder="Enter container name">
<!-- 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="deploy-container-name" class="form-label">Container Name <span class="text-danger">*</span></label>
<input type="text" id="deploy-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="deploy-image" class="form-label">Image <span class="text-danger">*</span></label>
<input type="text" id="deploy-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="deploy-command" class="form-label">Command Override</label>
<input type="text" id="deploy-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="deploy-entrypoint" class="form-label">Entrypoint Override</label>
<input type="text" id="deploy-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="deploy-workdir" class="form-label">Working Directory</label>
<input type="text" id="deploy-workdir" class="form-control bg-dark text-white" placeholder="/app">
<small class="text-muted">Container working directory</small>
</div>
</div>
</div>
<div class="mb-3">
<label for="deploy-image" class="form-label">Image</label>
<input type="text" id="deploy-image" class="form-control" required />
<!-- Accordion for Advanced Settings -->
<div class="accordion accordion-flush" id="deployAccordion">
<!-- 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="#networkingCollapse">
<i class="fas fa-network-wired me-2"></i> Networking
</button>
</h2>
<div id="networkingCollapse" class="accordion-collapse collapse" data-bs-parent="#deployAccordion">
<div class="accordion-body">
<div class="row mb-3">
<div class="col-md-6">
<label for="deploy-network-mode" class="form-label">Network Mode</label>
<select id="deploy-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="deploy-custom-network-container" style="display: none;">
<label for="deploy-custom-network" class="form-label">Custom Network</label>
<input type="text" id="deploy-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="deploy-hostname" class="form-label">Hostname</label>
<input type="text" id="deploy-hostname" class="form-control bg-dark text-white" placeholder="container-hostname">
</div>
<div class="col-md-6">
<label for="deploy-domainname" class="form-label">Domain Name</label>
<input type="text" id="deploy-domainname" class="form-control bg-dark text-white" placeholder="example.com">
</div>
</div>
<div class="mb-3">
<label class="form-label">Port Mappings</label>
<div id="deploy-ports-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addPortMapping()">
<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 class="form-label">DNS Servers</label>
<div id="deploy-dns-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDnsServer()">
<i class="fas fa-plus"></i> Add DNS Server
</button>
</div>
<div class="mb-3">
<label class="form-label">Extra Hosts</label>
<div id="deploy-extra-hosts-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addExtraHost()">
<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="#volumesCollapse">
<i class="fas fa-hdd me-2"></i> Volumes & Storage
</button>
</h2>
<div id="volumesCollapse" class="accordion-collapse collapse" data-bs-parent="#deployAccordion">
<div class="accordion-body">
<div class="mb-3">
<label class="form-label">Volume Mounts</label>
<div id="deploy-volumes-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addVolumeMount()">
<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 class="form-label">Tmpfs Mounts</label>
<div id="deploy-tmpfs-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addTmpfsMount()">
<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="#resourcesCollapse">
<i class="fas fa-server me-2"></i> Resources
</button>
</h2>
<div id="resourcesCollapse" class="accordion-collapse collapse" data-bs-parent="#deployAccordion">
<div class="accordion-body">
<div class="row mb-3">
<div class="col-md-4">
<label for="deploy-cpu-limit" class="form-label">CPU Limit (cores)</label>
<input type="number" id="deploy-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="deploy-cpu-reservation" class="form-label">CPU Reservation</label>
<input type="number" id="deploy-cpu-reservation" class="form-control bg-dark text-white" placeholder="1" step="0.1" min="0">
</div>
<div class="col-md-4">
<label for="deploy-cpu-shares" class="form-label">CPU Shares</label>
<input type="number" id="deploy-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="deploy-memory-limit" class="form-label">Memory Limit (MB)</label>
<input type="number" id="deploy-memory-limit" class="form-control bg-dark text-white" placeholder="512" min="0">
</div>
<div class="col-md-4">
<label for="deploy-memory-reservation" class="form-label">Memory Reservation (MB)</label>
<input type="number" id="deploy-memory-reservation" class="form-control bg-dark text-white" placeholder="256" min="0">
</div>
<div class="col-md-4">
<label for="deploy-memory-swap" class="form-label">Memory Swap (MB)</label>
<input type="number" id="deploy-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 class="form-label">Device Mappings</label>
<div id="deploy-devices-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addDeviceMapping()">
<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="#envLabelsCollapse">
<i class="fas fa-tags me-2"></i> Environment & Labels
</button>
</h2>
<div id="envLabelsCollapse" class="accordion-collapse collapse" data-bs-parent="#deployAccordion">
<div class="accordion-body">
<div class="mb-3">
<label class="form-label">Environment Variables</label>
<div id="deploy-env"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addEnvVar()">
<i class="fas fa-plus"></i> Add Variable
</button>
</div>
<div class="mb-3">
<label class="form-label">Labels</label>
<div id="deploy-labels-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addLabel()">
<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="#securityCollapse">
<i class="fas fa-shield-alt me-2"></i> Security
</button>
</h2>
<div id="securityCollapse" class="accordion-collapse collapse" data-bs-parent="#deployAccordion">
<div class="accordion-body">
<div class="row mb-3">
<div class="col-md-6">
<label for="deploy-user" class="form-label">User/UID</label>
<input type="text" id="deploy-user" class="form-control bg-dark text-white" placeholder="1000 or username">
</div>
<div class="col-md-6">
<label for="deploy-group" class="form-label">Group/GID</label>
<input type="text" id="deploy-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="deploy-privileged">
<label class="form-check-label" for="deploy-privileged">Privileged Mode</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="deploy-readonly-rootfs">
<label class="form-check-label" for="deploy-readonly-rootfs">Read-only Root Filesystem</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Capabilities</label>
<div id="deploy-capabilities-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addCapability()">
<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="deploy-security-opts-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addSecurityOpt()">
<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="#runtimeCollapse">
<i class="fas fa-play-circle me-2"></i> Runtime & Behavior
</button>
</h2>
<div id="runtimeCollapse" class="accordion-collapse collapse" data-bs-parent="#deployAccordion">
<div class="accordion-body">
<div class="row mb-3">
<div class="col-md-6">
<label for="deploy-restart-policy" class="form-label">Restart Policy</label>
<select id="deploy-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="deploy-restart-max-retries" class="form-label">Max Retries (on-failure)</label>
<input type="number" id="deploy-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="deploy-auto-remove">
<label class="form-check-label" for="deploy-auto-remove">Auto Remove</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="deploy-tty">
<label class="form-check-label" for="deploy-tty">TTY</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="deploy-stdin-open">
<label class="form-check-label" for="deploy-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="deploy-detach">
<label class="form-check-label" for="deploy-detach">Detach Mode</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="deploy-init">
<label class="form-check-label" for="deploy-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="#healthCollapse">
<i class="fas fa-heartbeat me-2"></i> Health & Logging
</button>
</h2>
<div id="healthCollapse" class="accordion-collapse collapse" data-bs-parent="#deployAccordion">
<div class="accordion-body">
<div class="mb-3">
<label for="deploy-health-cmd" class="form-label">Health Check Command</label>
<input type="text" id="deploy-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="deploy-health-interval" class="form-label">Interval (s)</label>
<input type="number" id="deploy-health-interval" class="form-control bg-dark text-white" placeholder="30" min="1">
</div>
<div class="col-md-3">
<label for="deploy-health-timeout" class="form-label">Timeout (s)</label>
<input type="number" id="deploy-health-timeout" class="form-control bg-dark text-white" placeholder="10" min="1">
</div>
<div class="col-md-3">
<label for="deploy-health-retries" class="form-label">Retries</label>
<input type="number" id="deploy-health-retries" class="form-control bg-dark text-white" placeholder="3" min="0">
</div>
<div class="col-md-3">
<label for="deploy-health-start-period" class="form-label">Start Period (s)</label>
<input type="number" id="deploy-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="deploy-log-driver" class="form-label">Logging Driver</label>
<select id="deploy-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="deploy-log-opts-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addLogOpt()">
<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="#advancedCollapse">
<i class="fas fa-cogs me-2"></i> Advanced
</button>
</h2>
<div id="advancedCollapse" class="accordion-collapse collapse" data-bs-parent="#deployAccordion">
<div class="accordion-body">
<div class="mb-3">
<label class="form-label">Sysctls</label>
<div id="deploy-sysctls-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addSysctl()">
<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="deploy-ulimits-container"></div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addUlimit()">
<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="deploy-oom-kill-disable">
<label class="form-check-label" for="deploy-oom-kill-disable">Disable OOM Killer</label>
</div>
</div>
<div class="col-md-6">
<label for="deploy-pids-limit" class="form-label">PIDs Limit</label>
<input type="number" id="deploy-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="deploy-shm-size" class="form-label">Shared Memory Size (MB)</label>
<input type="number" id="deploy-shm-size" class="form-control bg-dark text-white" placeholder="64" min="0">
</div>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="deploy-ports" class="form-label">Ports</label>
<input type="text" id="deploy-ports" class="form-control" placeholder="e.g., 80/tcp, 443/tcp" />
<!-- Preview Section -->
<div class="mt-4 mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="deploy-show-preview" onchange="togglePreview()">
<label class="form-check-label" for="deploy-show-preview">Show JSON Preview</label>
</div>
<div id="deploy-preview-container" style="display: none;" class="mt-2">
<pre id="deploy-preview" class="bg-dark text-white p-3 rounded" style="max-height: 300px; overflow-y: auto; font-size: 12px;"></pre>
</div>
</div>
<div class="mb-3">
<label for="deploy-volumes" class="form-label">Volumes</label>
<input type="text" id="deploy-volumes" class="form-control" placeholder="e.g., /host/path:/container/path" />
<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-rocket"></i> Deploy Container
</button>
</div>
<div class="mb-3">
<label for="deploy-env" class="form-label">Environment Variables</label>
<div id="deploy-env"></div>
</div>
<button type="submit" class="btn btn-primary">Deploy</button>
</form>
</div>
</div>
+579 -96
View File
@@ -4,14 +4,30 @@ const templateSearchInput = document.getElementById('template-search-input');
const templateDeployModal = new bootstrap.Modal(document.getElementById('templateDeployModalUnique'));
const deployForm = document.getElementById('deploy-form');
let templates = [];
// Array item counters for unique IDs
let portCounter = 0;
let volumeCounter = 0;
let envCounter = 0;
let labelCounter = 0;
let dnsCounter = 0;
let extraHostCounter = 0;
let deviceCounter = 0;
let capabilityCounter = 0;
let securityOptCounter = 0;
let logOptCounter = 0;
let sysctlCounter = 0;
let ulimitCounter = 0;
let tmpfsCounter = 0;
function closeAllModals() {
// Find and hide all open modals
const modals = document.querySelectorAll('.modal.show'); // Adjust selector if necessary
const modals = document.querySelectorAll('.modal.show');
modals.forEach(modal => {
const modalInstance = bootstrap.Modal.getInstance(modal); // Get Bootstrap modal instance
modalInstance.hide(); // Close the modal
const modalInstance = bootstrap.Modal.getInstance(modal);
modalInstance.hide();
});
}
// Show status indicator
function showStatusIndicator(message = 'Processing...') {
const statusIndicator = document.createElement('div');
@@ -32,10 +48,7 @@ function showStatusIndicator(message = 'Processing...') {
function hideStatusIndicator() {
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
console.log('[DEBUG] Hiding status indicator');
statusIndicator.remove();
} else {
console.error('[ERROR] Status indicator element not found!');
}
}
@@ -48,12 +61,11 @@ function showAlert(type, message) {
const container = document.querySelector('#alert-container');
if (container) {
container.appendChild(alertBox);
setTimeout(() => {
container.removeChild(alertBox);
if (container.contains(alertBox)) {
container.removeChild(alertBox);
}
}, 5000);
} else {
console.warn('[WARN] Alert container not found.');
}
}
@@ -65,7 +77,7 @@ async function fetchTemplates() {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
templates = data.templates || []; // Update global templates
templates = data.templates || [];
displayTemplateList(templates);
} catch (error) {
console.error('[ERROR] Failed to fetch templates:', error.message);
@@ -101,130 +113,601 @@ function displayTemplateList(templates) {
});
}
// Array management functions
function addPortMapping() {
const container = document.getElementById('deploy-ports-container');
const id = `port-${portCounter++}`;
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);
updatePreview();
}
function addVolumeMount() {
const container = document.getElementById('deploy-volumes-container');
const id = `volume-${volumeCounter++}`;
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);
updatePreview();
}
function addTmpfsMount() {
const container = document.getElementById('deploy-tmpfs-container');
const id = `tmpfs-${tmpfsCounter++}`;
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);
updatePreview();
}
function addEnvVar() {
const container = document.getElementById('deploy-env');
const id = `env-${envCounter++}`;
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);
updatePreview();
}
function addLabel() {
const container = document.getElementById('deploy-labels-container');
const id = `label-${labelCounter++}`;
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);
updatePreview();
}
function addDnsServer() {
const container = document.getElementById('deploy-dns-container');
const id = `dns-${dnsCounter++}`;
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);
updatePreview();
}
function addExtraHost() {
const container = document.getElementById('deploy-extra-hosts-container');
const id = `host-${extraHostCounter++}`;
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);
updatePreview();
}
function addDeviceMapping() {
const container = document.getElementById('deploy-devices-container');
const id = `device-${deviceCounter++}`;
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);
updatePreview();
}
function addCapability() {
const container = document.getElementById('deploy-capabilities-container');
const id = `cap-${capabilityCounter++}`;
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);
updatePreview();
}
function addSecurityOpt() {
const container = document.getElementById('deploy-security-opts-container');
const id = `secopt-${securityOptCounter++}`;
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);
updatePreview();
}
function addLogOpt() {
const container = document.getElementById('deploy-log-opts-container');
const id = `logopt-${logOptCounter++}`;
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);
updatePreview();
}
function addSysctl() {
const container = document.getElementById('deploy-sysctls-container');
const id = `sysctl-${sysctlCounter++}`;
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);
updatePreview();
}
function addUlimit() {
const container = document.getElementById('deploy-ulimits-container');
const id = `ulimit-${ulimitCounter++}`;
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);
updatePreview();
}
function removeArrayItem(id) {
const item = document.getElementById(id);
if (item) {
item.remove();
updatePreview();
}
}
// Make functions globally available
window.addPortMapping = addPortMapping;
window.addVolumeMount = addVolumeMount;
window.addTmpfsMount = addTmpfsMount;
window.addEnvVar = addEnvVar;
window.addLabel = addLabel;
window.addDnsServer = addDnsServer;
window.addExtraHost = addExtraHost;
window.addDeviceMapping = addDeviceMapping;
window.addCapability = addCapability;
window.addSecurityOpt = addSecurityOpt;
window.addLogOpt = addLogOpt;
window.addSysctl = addSysctl;
window.addUlimit = addUlimit;
window.removeArrayItem = removeArrayItem;
// Network mode change handler
document.addEventListener('DOMContentLoaded', () => {
const networkMode = document.getElementById('deploy-network-mode');
const customNetworkContainer = document.getElementById('deploy-custom-network-container');
if (networkMode && customNetworkContainer) {
networkMode.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';
}
updatePreview();
});
}
// Add input listeners for preview updates
const form = document.getElementById('deploy-form');
if (form) {
form.addEventListener('input', () => updatePreview());
form.addEventListener('change', () => updatePreview());
}
});
// Collect all form data
function collectFormData() {
const data = {
containerName: document.getElementById('deploy-container-name')?.value.trim() || '',
image: document.getElementById('deploy-image')?.value.trim() || '',
command: document.getElementById('deploy-command')?.value.trim() || null,
entrypoint: document.getElementById('deploy-entrypoint')?.value.trim() || null,
workingDir: document.getElementById('deploy-workdir')?.value.trim() || null,
// Networking
networkMode: document.getElementById('deploy-network-mode')?.value || 'bridge',
customNetwork: (() => {
const mode = document.getElementById('deploy-network-mode')?.value;
const customNet = document.getElementById('deploy-custom-network')?.value.trim();
if ((mode === 'container' || (mode && mode !== 'bridge' && mode !== 'host' && mode !== 'none')) && customNet) {
return customNet;
}
return null;
})(),
hostname: document.getElementById('deploy-hostname')?.value.trim() || null,
domainname: document.getElementById('deploy-domainname')?.value.trim() || null,
ports: collectArrayItems('deploy-ports-container', 'data-port-id'),
dns: collectArrayItems('deploy-dns-container', 'data-dns-id'),
extraHosts: collectArrayItems('deploy-extra-hosts-container', 'data-host-id'),
// Volumes
volumes: collectArrayItems('deploy-volumes-container', 'data-volume-id'),
tmpfs: collectArrayItems('deploy-tmpfs-container', 'data-tmpfs-id'),
// Resources
cpuLimit: parseFloat(document.getElementById('deploy-cpu-limit')?.value) || null,
cpuReservation: parseFloat(document.getElementById('deploy-cpu-reservation')?.value) || null,
cpuShares: parseInt(document.getElementById('deploy-cpu-shares')?.value) || null,
memoryLimit: parseInt(document.getElementById('deploy-memory-limit')?.value) || null,
memoryReservation: parseInt(document.getElementById('deploy-memory-reservation')?.value) || null,
memorySwap: parseInt(document.getElementById('deploy-memory-swap')?.value) || null,
devices: collectArrayItems('deploy-devices-container', 'data-device-id'),
// Environment & Labels
env: collectEnvVars(),
labels: collectLabels(),
// Security
user: document.getElementById('deploy-user')?.value.trim() || null,
group: document.getElementById('deploy-group')?.value.trim() || null,
privileged: document.getElementById('deploy-privileged')?.checked || false,
readonlyRootfs: document.getElementById('deploy-readonly-rootfs')?.checked || false,
capabilities: collectArrayItems('deploy-capabilities-container', 'data-cap-id'),
securityOpts: collectArrayItems('deploy-security-opts-container', 'data-secopt-id'),
// Runtime
restartPolicy: document.getElementById('deploy-restart-policy')?.value || 'no',
restartMaxRetries: parseInt(document.getElementById('deploy-restart-max-retries')?.value) || null,
autoRemove: document.getElementById('deploy-auto-remove')?.checked || false,
tty: document.getElementById('deploy-tty')?.checked || false,
stdinOpen: document.getElementById('deploy-stdin-open')?.checked || false,
detach: document.getElementById('deploy-detach')?.checked !== false, // Default true
init: document.getElementById('deploy-init')?.checked || false,
// Health & Logging
healthCmd: document.getElementById('deploy-health-cmd')?.value.trim() || null,
healthInterval: parseInt(document.getElementById('deploy-health-interval')?.value) || null,
healthTimeout: parseInt(document.getElementById('deploy-health-timeout')?.value) || null,
healthRetries: parseInt(document.getElementById('deploy-health-retries')?.value) || null,
healthStartPeriod: parseInt(document.getElementById('deploy-health-start-period')?.value) || null,
logDriver: document.getElementById('deploy-log-driver')?.value || null,
logOpts: collectLogOpts(),
// Advanced
sysctls: collectSysctls(),
ulimits: collectUlimits(),
oomKillDisable: document.getElementById('deploy-oom-kill-disable')?.checked || false,
pidsLimit: parseInt(document.getElementById('deploy-pids-limit')?.value) || null,
shmSize: parseInt(document.getElementById('deploy-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 collectArrayItems(containerId, dataAttr) {
const container = document.getElementById(containerId);
if (!container) return [];
const items = [];
container.querySelectorAll(`[${dataAttr}]`).forEach(input => {
const value = input.value.trim();
if (value) items.push(value);
});
return items;
}
function collectEnvVars() {
const container = document.getElementById('deploy-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 collectLabels() {
const items = collectArrayItems('deploy-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 collectLogOpts() {
const container = document.getElementById('deploy-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 collectSysctls() {
const items = collectArrayItems('deploy-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 collectUlimits() {
const items = collectArrayItems('deploy-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;
}
// Update preview
function updatePreview() {
const previewContainer = document.getElementById('deploy-preview-container');
const preview = document.getElementById('deploy-preview');
const showPreview = document.getElementById('deploy-show-preview')?.checked;
if (!previewContainer || !preview) return;
if (showPreview) {
const data = collectFormData();
preview.textContent = JSON.stringify(data, null, 2);
previewContainer.style.display = 'block';
} else {
previewContainer.style.display = 'none';
}
}
window.togglePreview = updatePreview;
// Open deploy modal and populate the form dynamically
function openDeployModal(template) {
console.log('[DEBUG] Opening deploy modal for:', template);
// Reset counters
portCounter = volumeCounter = envCounter = labelCounter = dnsCounter = 0;
extraHostCounter = deviceCounter = capabilityCounter = securityOptCounter = 0;
logOptCounter = sysctlCounter = ulimitCounter = tmpfsCounter = 0;
// Set the modal title
const deployTitle = document.getElementById('deploy-title');
deployTitle.textContent = `Deploy ${template.title}`;
if (deployTitle) deployTitle.textContent = `Deploy ${template.title}`;
// Populate the image name
const deployImage = document.getElementById('deploy-image');
deployImage.value = template.image || '';
// Populate ports
const deployPorts = document.getElementById('deploy-ports');
deployPorts.value = (template.ports || []).join(', ');
// Populate volumes
const deployVolumes = document.getElementById('deploy-volumes');
deployVolumes.value = (template.volumes || [])
.map(volume => `${volume.bind}:${volume.container}`)
.join(', ');
// Add environment variables
const deployEnv = document.getElementById('deploy-env');
deployEnv.innerHTML = '';
(template.env || []).forEach(env => {
const envRow = document.createElement('div');
envRow.className = 'mb-3';
envRow.innerHTML = `
<label>${env.label || env.name}</label>
<input type="text" class="form-control" data-env-name="${env.name}" value="${env.default || ''}">
`;
deployEnv.appendChild(envRow);
// Clear all form fields
const form = document.getElementById('deploy-form');
if (form) form.reset();
// Clear all array containers
['deploy-ports-container', 'deploy-volumes-container', 'deploy-env', 'deploy-labels-container',
'deploy-dns-container', 'deploy-extra-hosts-container', 'deploy-devices-container',
'deploy-capabilities-container', 'deploy-security-opts-container', 'deploy-log-opts-container',
'deploy-sysctls-container', 'deploy-ulimits-container', 'deploy-tmpfs-container'].forEach(id => {
const container = document.getElementById(id);
if (container) container.innerHTML = '';
});
// Add Container Name field
const containerNameField = document.getElementById('deploy-container-name');
containerNameField.value = ''; // Clear previous value, if any
// Populate basic fields
const deployImage = document.getElementById('deploy-image');
if (deployImage) deployImage.value = template.image || '';
// Populate ports from template
if (template.ports && Array.isArray(template.ports)) {
template.ports.forEach(port => {
addPortMapping();
const container = document.getElementById('deploy-ports-container');
const lastInput = container?.lastElementChild?.querySelector('input');
if (lastInput) lastInput.value = port;
});
}
// Populate volumes from template
if (template.volumes && Array.isArray(template.volumes)) {
template.volumes.forEach(volume => {
addVolumeMount();
const container = document.getElementById('deploy-volumes-container');
const lastInput = container?.lastElementChild?.querySelector('input');
if (lastInput && volume.bind && volume.container) {
lastInput.value = `${volume.bind}:${volume.container}${volume.mode ? ':' + volume.mode : ''}`;
}
});
}
// Populate environment variables from template
if (template.env && Array.isArray(template.env)) {
template.env.forEach(env => {
addEnvVar();
const container = document.getElementById('deploy-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 = env.name || '';
if (valueInput) valueInput.value = env.default || '';
}
});
}
// Show the modal
templateDeployModal.show();
updatePreview();
}
// Validate form data
function validateFormData(data) {
const errors = [];
if (!data.containerName || !/^[a-zA-Z0-9-_]+$/.test(data.containerName)) {
errors.push('Container name must be alphanumeric and may include dashes or underscores.');
}
if (!data.image) {
errors.push('Image name is required.');
}
// Validate ports
data.ports?.forEach((port, idx) => {
if (!/^\d+:\d+\/(tcp|udp)$/.test(port) && !/^\d+\/(tcp|udp)$/.test(port)) {
errors.push(`Port ${idx + 1} has invalid format. Use "host:container/protocol" or "container/protocol".`);
}
});
// Validate volumes
data.volumes?.forEach((volume, idx) => {
if (!volume.includes(':')) {
errors.push(`Volume ${idx + 1} must contain a colon (host:container).`);
}
});
return errors;
}
// Deploy Docker container
async function deployDockerContainer(payload) {
const { containerName, imageName, ports = [], volumes = [], envVars = [] } = payload;
const validPorts = ports.filter(port => {
if (!port || !port.includes('/')) {
console.warn(`[WARN] Invalid port entry skipped: ${port}`);
return false;
}
return true;
});
const validVolumes = volumes.filter(volume => {
if (!volume || !volume.includes(':')) {
console.warn(`[WARN] Invalid volume entry skipped: ${volume}`);
return false;
}
return true;
});
console.log('[INFO] Sending deployment command to the server...');
return new Promise((resolve, reject) => {
window.handlePeerResponse = (response) => {
if (response.success && response.message.includes('deployed successfully')) {
console.log('[INFO] Deployment response received:', response.message);
resolve(response); // Resolve with success
resolve(response);
} else if (response.error) {
reject(new Error(response.error)); // Reject on error
reject(new Error(response.error));
}
};
// Send the deployment command
sendCommand('deployContainer', {
containerName,
image: imageName,
ports: validPorts,
volumes: validVolumes,
env: envVars.map(({ name, value }) => ({ name, value })),
});
sendCommand('deployContainer', payload);
// Fallback timeout to avoid hanging indefinitely
setTimeout(() => {
reject(new Error('Deployment timed out. No response from server.'));
}, 30000); // Adjust timeout duration as needed
}, 60000); // Increased timeout for complex deployments
});
}
// Handle form submission for deployment
deployForm.addEventListener('submit', async (e) => {
closeAllModals();
e.preventDefault();
const containerName = document.getElementById('deploy-container-name').value.trim();
const imageName = document.getElementById('deploy-image').value.trim();
const ports = document.getElementById('deploy-ports').value.split(',').map(port => port.trim());
const volumes = document.getElementById('deploy-volumes').value.split(',').map(volume => volume.trim());
const envInputs = document.querySelectorAll('#deploy-env input');
const envVars = Array.from(envInputs).map(input => ({
name: input.getAttribute('data-env-name'),
value: input.value.trim(),
}));
const deployPayload = { containerName, imageName, ports, volumes, envVars };
console.log('[DEBUG] Deploy payload:', deployPayload);
const formData = collectFormData();
// Validate
const errors = validateFormData(formData);
if (errors.length > 0) {
showAlert('danger', errors.join(' '));
return;
}
try {
showStatusIndicator('Deploying container...');
// Wait for deployment to complete
const successResponse = await deployDockerContainer(deployPayload);
const successResponse = await deployDockerContainer(formData);
hideStatusIndicator();
// showAlert('success', successResponse.message);
closeAllModals();
showAlert('success', successResponse.message || 'Container deployed successfully!');
} catch (error) {
console.error('[ERROR] Failed to deploy container:', error.message);
hideStatusIndicator();
closeAllModals();
showAlert('danger', error.message);
}
});
@@ -233,4 +716,4 @@ deployForm.addEventListener('submit', async (e) => {
document.addEventListener('DOMContentLoaded', fetchTemplates);
// Export required functions
export { fetchTemplates, displayTemplateList, openDeployModal };
export { fetchTemplates, displayTemplateList, openDeployModal };
+273 -65
View File
@@ -264,96 +264,281 @@ swarm.on('connection', (peer) => {
case 'deployContainer':
console.log('[INFO] Handling "deployContainer" command');
const { containerName, image: imageToDeploy, ports = [], volumes = [], env = [] } = parsedData.args;
const args = parsedData.args;
try {
// Validate and sanitize container name
if (!containerName || typeof containerName !== 'string') {
if (!args.containerName || typeof args.containerName !== 'string') {
throw new Error('Invalid or missing container name.');
}
// Ensure the name is alphanumeric with optional dashes/underscores
if (!/^[a-zA-Z0-9-_]+$/.test(containerName)) {
if (!/^[a-zA-Z0-9-_]+$/.test(args.containerName)) {
throw new Error('Container name must be alphanumeric and may include dashes or underscores.');
}
// Validate and sanitize image
if (!imageToDeploy || typeof imageToDeploy !== 'string') {
if (!args.image || typeof args.image !== 'string') {
throw new Error('Invalid or missing Docker image.');
}
// Validate and sanitize ports
const validPorts = ports.filter((port) => {
if (typeof port === 'string' && /^\d+\/(tcp|udp)$/.test(port)) {
return true;
} else {
console.warn(`[WARN] Invalid port entry skipped: ${port}`);
return false;
}
});
// Check if container name already exists
const existingContainers = await docker.listContainers({ all: true });
const nameExists = existingContainers.some(c => c.Names.includes(`/${args.containerName}`));
if (nameExists) {
throw new Error(`Container name '${args.containerName}' already exists.`);
}
// Validate and sanitize volumes
const validVolumes = volumes.filter((volume) => {
if (typeof volume === 'string' && volume.includes(':')) {
return true;
} else {
console.warn(`[WARN] Invalid volume entry skipped: ${volume}`);
return false;
}
});
// Validate and sanitize environment variables
const validEnv = env
.map(({ name, value }) => {
if (name && value) {
return `${name}=${value}`;
} else {
console.warn(`[WARN] Invalid environment variable skipped: name=${name}, value=${value}`);
return null;
}
})
.filter(Boolean);
console.log(`[INFO] Pulling Docker image "${imageToDeploy}"`);
console.log(`[INFO] Pulling Docker image "${args.image}"`);
// Pull the Docker image
const pullStream = await docker.pull(imageToDeploy);
const pullStream = await docker.pull(args.image);
await new Promise((resolve, reject) => {
docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve()));
});
console.log(`[INFO] Image "${imageToDeploy}" pulled successfully`);
console.log(`[INFO] Image "${args.image}" pulled successfully`);
// Configure container creation settings
const hostConfig = {
PortBindings: {},
Binds: validVolumes, // Use validated volumes in Docker's expected format
NetworkMode: 'bridge', // Set the network mode to bridge
// Build container configuration
const containerConfig = {
name: args.containerName,
Image: args.image,
};
validPorts.forEach((port) => {
const [containerPort, protocol] = port.split('/');
hostConfig.PortBindings[`${containerPort}/${protocol}`] = [{ HostPort: containerPort }];
});
// Create and start the container with a custom name
// Basic settings
if (args.command) {
containerConfig.Cmd = args.command.split(' ');
}
if (args.entrypoint) {
containerConfig.Entrypoint = args.entrypoint.split(' ');
}
if (args.workingDir) {
containerConfig.WorkingDir = args.workingDir;
}
// Environment variables
if (args.env && Array.isArray(args.env)) {
containerConfig.Env = args.env
.filter(e => e.name && e.value !== undefined)
.map(e => `${e.name}=${e.value}`);
}
// Labels
if (args.labels && typeof args.labels === 'object') {
containerConfig.Labels = args.labels;
}
// Hostname and domainname
if (args.hostname) containerConfig.Hostname = args.hostname;
if (args.domainname) containerConfig.Domainname = args.domainname;
// User
if (args.user) containerConfig.User = args.user;
// Health check
if (args.healthCmd) {
containerConfig.Healthcheck = {
Test: args.healthCmd.startsWith('CMD-SHELL')
? args.healthCmd.split(' ').slice(1)
: ['CMD-SHELL', args.healthCmd],
Interval: args.healthInterval ? args.healthInterval * 1000000000 : 30000000000, // nanoseconds
Timeout: args.healthTimeout ? args.healthTimeout * 1000000000 : 10000000000,
Retries: args.healthRetries || 3,
StartPeriod: args.healthStartPeriod ? args.healthStartPeriod * 1000000000 : 0,
};
}
// TTY and Stdin
containerConfig.Tty = args.tty === true;
containerConfig.OpenStdin = args.stdinOpen === true;
containerConfig.AttachStdin = args.stdinOpen === true;
containerConfig.AttachStdout = true;
containerConfig.AttachStderr = true;
// Read-only root filesystem
if (args.readonlyRootfs === true) {
containerConfig.ReadonlyRootfs = true;
}
// Build HostConfig
const hostConfig = {
NetworkMode: args.networkMode || 'bridge',
};
// Port bindings
if (args.ports && Array.isArray(args.ports)) {
hostConfig.PortBindings = {};
args.ports.forEach((portStr) => {
// Support both "host:container/protocol" and "container/protocol" formats
if (portStr.includes(':')) {
const [hostPort, rest] = portStr.split(':');
const [containerPort, protocol] = rest.split('/');
hostConfig.PortBindings[`${containerPort}/${protocol || 'tcp'}`] = [{ HostPort: hostPort }];
} else {
const [containerPort, protocol] = portStr.split('/');
hostConfig.PortBindings[`${containerPort}/${protocol || 'tcp'}`] = [{ HostPort: containerPort }];
}
});
}
// Volumes
if (args.volumes && Array.isArray(args.volumes)) {
hostConfig.Binds = args.volumes.filter(v => v && v.includes(':'));
}
// Tmpfs
if (args.tmpfs && Array.isArray(args.tmpfs)) {
hostConfig.Tmpfs = {};
args.tmpfs.forEach(tmpfsStr => {
const [path, ...opts] = tmpfsStr.split(':');
if (path) {
hostConfig.Tmpfs[path] = opts.join(':') || '';
}
});
}
// Resources
if (args.cpuLimit) {
hostConfig.NanoCpus = args.cpuLimit * 1000000000; // Convert to nanoseconds
}
if (args.cpuReservation) {
hostConfig.CpuQuota = args.cpuReservation * 1000000000;
}
if (args.cpuShares) {
hostConfig.CpuShares = args.cpuShares;
}
if (args.memoryLimit) {
hostConfig.Memory = args.memoryLimit * 1024 * 1024; // Convert MB to bytes
}
if (args.memoryReservation) {
hostConfig.MemoryReservation = args.memoryReservation * 1024 * 1024;
}
if (args.memorySwap !== undefined && args.memorySwap !== null) {
hostConfig.MemorySwap = args.memorySwap === -1 ? -1 : args.memorySwap * 1024 * 1024;
}
// Devices
if (args.devices && Array.isArray(args.devices)) {
hostConfig.Devices = args.devices.map(deviceStr => {
const parts = deviceStr.split(':');
return {
PathOnHost: parts[0],
PathInContainer: parts[1] || parts[0],
CgroupPermissions: parts[2] || 'rwm'
};
});
}
// DNS
if (args.dns && Array.isArray(args.dns)) {
hostConfig.Dns = args.dns;
}
// Extra hosts
if (args.extraHosts && Array.isArray(args.extraHosts)) {
hostConfig.ExtraHosts = args.extraHosts;
}
// Restart policy
if (args.restartPolicy) {
hostConfig.RestartPolicy = {
Name: args.restartPolicy,
MaximumRetryCount: args.restartMaxRetries || 0
};
}
// Auto remove
if (args.autoRemove === true) {
hostConfig.AutoRemove = true;
}
// Privileged
if (args.privileged === true) {
hostConfig.Privileged = true;
}
// Capabilities
if (args.capabilities && Array.isArray(args.capabilities)) {
hostConfig.CapAdd = args.capabilities;
}
// Security options
if (args.securityOpts && Array.isArray(args.securityOpts)) {
hostConfig.SecurityOpt = args.securityOpts;
}
// Sysctls
if (args.sysctls && typeof args.sysctls === 'object') {
hostConfig.Sysctls = args.sysctls;
}
// Ulimits
if (args.ulimits && Array.isArray(args.ulimits)) {
hostConfig.Ulimits = args.ulimits;
}
// OOM kill disable
if (args.oomKillDisable === true) {
hostConfig.OomKillDisable = true;
}
// PIDs limit
if (args.pidsLimit !== undefined && args.pidsLimit !== null) {
hostConfig.PidsLimit = args.pidsLimit === -1 ? 0 : args.pidsLimit;
}
// Shared memory size
if (args.shmSize) {
hostConfig.ShmSize = args.shmSize * 1024 * 1024; // Convert MB to bytes
}
// Init process
if (args.init === true) {
hostConfig.Init = true;
}
// Logging
if (args.logDriver) {
hostConfig.LogConfig = {
Type: args.logDriver,
Config: args.logOpts || {}
};
}
// Custom network
if (args.networkMode === 'container' && args.customNetwork) {
hostConfig.NetworkMode = `container:${args.customNetwork}`;
} else if (args.customNetwork && args.networkMode !== 'host' && args.networkMode !== 'none') {
// Will be handled after container creation
}
containerConfig.HostConfig = hostConfig;
// Create the container
console.log('[INFO] Creating the container...');
const container = await docker.createContainer({
name: containerName, // Include the container name
Image: imageToDeploy,
Env: validEnv,
HostConfig: hostConfig,
});
const container = await docker.createContainer(containerConfig);
// Connect to custom network if specified
if (args.customNetwork && args.networkMode !== 'container' && args.networkMode !== 'host' && args.networkMode !== 'none') {
try {
const network = docker.getNetwork(args.customNetwork);
await network.connect({ Container: container.id });
console.log(`[INFO] Connected container to network: ${args.customNetwork}`);
} catch (netErr) {
console.warn(`[WARN] Failed to connect to network ${args.customNetwork}: ${netErr.message}`);
}
}
// Start the container
console.log('[INFO] Starting the container...');
await container.start();
console.log(`[INFO] Container "${containerName}" deployed successfully from image "${imageToDeploy}"`);
console.log(`[INFO] Container "${args.containerName}" deployed successfully`);
// Respond with success message
peer.write(
JSON.stringify({
success: true,
message: `Container "${containerName}" deployed successfully from image "${imageToDeploy}"`,
message: `Container "${args.containerName}" deployed successfully from image "${args.image}"`,
})
);
@@ -362,7 +547,11 @@ swarm.on('connection', (peer) => {
const update = { type: 'containers', data: containers };
for (const connectedPeer of connectedPeers) {
connectedPeer.write(JSON.stringify(update));
try {
connectedPeer.write(JSON.stringify(update));
} catch (peerErr) {
console.error(`[ERROR] Failed to send update to peer: ${peerErr.message}`);
}
}
} catch (err) {
console.error(`[ERROR] Failed to deploy container: ${err.message}`);
@@ -602,13 +791,32 @@ docker.listContainers({ all: true }, async (err, containers) => {
// Function to calculate CPU usage percentage
function calculateCPUPercent(stats) {
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
const cpuCount = stats.cpu_stats.online_cpus || stats.cpu_stats.cpu_usage.percpu_usage.length;
if (systemDelta > 0.0 && cpuDelta > 0.0) {
return (cpuDelta / systemDelta) * cpuCount * 100.0;
try {
// Validate required stats structure
if (!stats || !stats.cpu_stats || !stats.precpu_stats ||
!stats.cpu_stats.cpu_usage || !stats.precpu_stats.cpu_usage) {
return 0.0;
}
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
// Safely determine CPU count with multiple fallbacks
let cpuCount = 1; // Default to 1 CPU
if (stats.cpu_stats.online_cpus) {
cpuCount = stats.cpu_stats.online_cpus;
} else if (stats.cpu_stats.cpu_usage.percpu_usage && Array.isArray(stats.cpu_stats.cpu_usage.percpu_usage)) {
cpuCount = stats.cpu_stats.cpu_usage.percpu_usage.length;
}
if (systemDelta > 0.0 && cpuDelta > 0.0) {
return (cpuDelta / systemDelta) * cpuCount * 100.0;
}
return 0.0;
} catch (err) {
console.error(`[ERROR] Failed to calculate CPU percent: ${err.message}`);
return 0.0;
}
return 0.0;
}
// Function to handle terminal sessions
@@ -853,4 +1061,4 @@ process.on('SIGINT', () => {
swarm.destroy();
process.exit();
});
});