updating deployer
This commit is contained in:
+486
-19
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user