Update Containers.
This commit is contained in:
parent
e99cceaa52
commit
7b08e6a45e
@ -17,6 +17,12 @@ class ContainerService(BaseService):
|
|||||||
async def get_container_name(self, channel_uid):
|
async def get_container_name(self, channel_uid):
|
||||||
return f"channel-{channel_uid}"
|
return f"channel-{channel_uid}"
|
||||||
|
|
||||||
|
async def get(self,channel_uid):
|
||||||
|
return await self.compose.get_instance(await self.get_container_name(channel_uid))
|
||||||
|
|
||||||
|
async def get_status(self, channel_uid):
|
||||||
|
return await self.compose.get_instance_status(await self.get_container_name(channel_uid))
|
||||||
|
|
||||||
async def create(
|
async def create(
|
||||||
self,
|
self,
|
||||||
channel_uid,
|
channel_uid,
|
||||||
@ -61,11 +67,3 @@ class ContainerService(BaseService):
|
|||||||
return model
|
return model
|
||||||
raise Exception(f"Failed to create container: {model.errors}")
|
raise Exception(f"Failed to create container: {model.errors}")
|
||||||
|
|
||||||
async def get(self, id):
|
|
||||||
return await self.mapper.get(id)
|
|
||||||
|
|
||||||
async def update(self, model):
|
|
||||||
return await self.mapper.update(model)
|
|
||||||
|
|
||||||
async def delete(self, id):
|
|
||||||
return await self.mapper.delete(id)
|
|
||||||
|
@ -591,7 +591,31 @@ dialog .dialog-button.secondary {
|
|||||||
dialog .dialog-button.secondary:hover {
|
dialog .dialog-button.secondary:hover {
|
||||||
background-color: #f0b84c;
|
background-color: #f0b84c;
|
||||||
}
|
}
|
||||||
|
dialog .dialog-button.primary:disabled,
|
||||||
|
dialog .dialog-button.primary[aria-disabled="true"] {
|
||||||
|
/* slightly darker + lower saturation of the live colour */
|
||||||
|
background-color: #70321e; /* muted burnt orange */
|
||||||
|
color: #bfbfbf; /* light grey text */
|
||||||
|
|
||||||
|
opacity: .55; /* unified fade */
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- SECONDARY (yellow) ---------- */
|
||||||
|
dialog .dialog-button.secondary:disabled,
|
||||||
|
dialog .dialog-button.secondary[aria-disabled="true"] {
|
||||||
|
background-color: #6c5619; /* muted mustard */
|
||||||
|
color: #bfbfbf;
|
||||||
|
|
||||||
|
opacity: .55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog .dialog-button:disabled:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.embed-url-link {
|
.embed-url-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -633,4 +657,8 @@ dialog .dialog-button.secondary:hover {
|
|||||||
color: #f05a28;
|
color: #f05a28;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
@ -2,6 +2,10 @@ import copy
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import copy
|
||||||
|
import asyncio
|
||||||
|
import subprocess
|
||||||
|
|
||||||
class ComposeFileManager:
|
class ComposeFileManager:
|
||||||
def __init__(self, compose_path="docker-compose.yml"):
|
def __init__(self, compose_path="docker-compose.yml"):
|
||||||
@ -58,8 +62,14 @@ class ComposeFileManager:
|
|||||||
del self.compose["services"][name]
|
del self.compose["services"][name]
|
||||||
self._save()
|
self._save()
|
||||||
|
|
||||||
def get_instance(self, name):
|
async def get_instance(self, name):
|
||||||
return self.compose.get("services", {}).get(name)
|
instance = self.compose.get("services", {}).get(name)
|
||||||
|
print(self.compose.get("services", {}))
|
||||||
|
print(instance)
|
||||||
|
instance['status'] = await self.get_instance_status(name)
|
||||||
|
print("INSTANCE",instance)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
def duplicate_instance(self, name, new_name):
|
def duplicate_instance(self, name, new_name):
|
||||||
orig = self.get_instance(name)
|
orig = self.get_instance(name)
|
||||||
@ -78,6 +88,18 @@ class ComposeFileManager:
|
|||||||
self.compose["services"][name] = service
|
self.compose["services"][name] = service
|
||||||
self._save()
|
self._save()
|
||||||
|
|
||||||
|
async def get_instance_status(self, name):
|
||||||
|
"""Asynchronously check the status of a docker-compose service instance."""
|
||||||
|
if name not in self.list_instances():
|
||||||
|
return "error"
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"docker", "compose", "-f", self.compose_path, "ps", "--services", "--filter", f"status=running",
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
stdout, _ = await proc.communicate()
|
||||||
|
running_services = stdout.decode().split()
|
||||||
|
return "running" if name in running_services else "stopped"
|
||||||
# Storage size is not tracked in compose files; would need Docker API for that.
|
# Storage size is not tracked in compose files; would need Docker API for that.
|
||||||
|
|
||||||
|
|
||||||
|
185
src/snek/templates/dialog_container.html
Normal file
185
src/snek/templates/dialog_container.html
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<script>
|
||||||
|
class ContainerDialogComponent extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._channelUid = null;
|
||||||
|
this._dialog = null;
|
||||||
|
this._closeButton = null;
|
||||||
|
this._startButton = null;
|
||||||
|
this._restartButton = null;
|
||||||
|
this._stopButton = null;
|
||||||
|
this._statusSpan = null;
|
||||||
|
this._actions = null;
|
||||||
|
this._containerList = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this._channelUid = this.getAttribute('channel');
|
||||||
|
// Render template
|
||||||
|
this.innerHTML = `
|
||||||
|
<dialog class="container-dialog">
|
||||||
|
<div class="dialog-backdrop">
|
||||||
|
<div class="dialog-box">
|
||||||
|
<div class="dialog-title"><h2>Container</h2></div>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<table class="container-properties">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<td><span class="container-name"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Image</th>
|
||||||
|
<td><span class="container-image"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>CPUs</th>
|
||||||
|
<td><span class="container-cpus"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Memory</th>
|
||||||
|
<td><span class="container-memory"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<td><span class="container-status"></span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
|
||||||
|
<button class="dialog-button secondary btn-container-restart">Restart</button>
|
||||||
|
<button class="dialog-button secondary btn-container-stop">Stop</button>
|
||||||
|
<button class="dialog-button secondary btn-container-start">Start</button>
|
||||||
|
<button class="dialog-button primary btn-container-close">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this._dialog = this.querySelector('dialog');
|
||||||
|
this._closeButton = this.querySelector('.btn-container-close');
|
||||||
|
this._startButton = this.querySelector('.btn-container-start');
|
||||||
|
this._restartButton = this.querySelector('.btn-container-restart');
|
||||||
|
this._stopButton = this.querySelector('.btn-container-stop');
|
||||||
|
this._statusSpan = this.querySelector('.container-status');
|
||||||
|
this._imageSpan = this.querySelector('.container-image');
|
||||||
|
this._memorySpan = this.querySelector('.container-memory');
|
||||||
|
this._cpusSpan = this.querySelector('.container-cpus');
|
||||||
|
this._nameSpan = this.querySelector('.container-name');
|
||||||
|
this._actions = this.querySelector('.dialog-actions');
|
||||||
|
|
||||||
|
this._initEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
set channelUid(val) {
|
||||||
|
this._channelUid = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
get channelUid() {
|
||||||
|
return this._channelUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
_initEventListeners() {
|
||||||
|
if (this._closeButton) {
|
||||||
|
this._closeButton.addEventListener('click', () => this.close());
|
||||||
|
}
|
||||||
|
if (this._startButton) {
|
||||||
|
this._startButton.addEventListener('click', () => this.startContainer());
|
||||||
|
}
|
||||||
|
if (this._restartButton) {
|
||||||
|
this._restartButton.addEventListener('click', () => this.restartContainer());
|
||||||
|
}
|
||||||
|
if (this._stopButton) {
|
||||||
|
this._stopButton.addEventListener('click', () => this.stopContainer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async openWithStatus() {
|
||||||
|
if (!this._channelUid) return;
|
||||||
|
const container = await (window.app?.rpc?.getContainer?.(this._channelUid) ?? Promise.resolve('unknown'));
|
||||||
|
this.updateContainerInfo(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContainerInfo(container) {
|
||||||
|
this.updateStatus(container['status']);
|
||||||
|
this.updateCpus(container['cpus']);
|
||||||
|
this.updateMemory(container['memory']);
|
||||||
|
this.updateImage(container['image']);
|
||||||
|
this.updateName(container['name']);
|
||||||
|
this._dialog.showModal();
|
||||||
|
this._closeButton.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(status) {
|
||||||
|
this._statusSpan.innerText = status;
|
||||||
|
if (status === 'running') {
|
||||||
|
this._stopButton.disabled = false;
|
||||||
|
this._startButton.disabled = true;
|
||||||
|
this._restartButton.disabled = false;
|
||||||
|
} else {
|
||||||
|
this._stopButton.disabled = true;
|
||||||
|
this._startButton.disabled = false;
|
||||||
|
this._restartButton.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCpus(cpus) {
|
||||||
|
if (this._cpusSpan) {
|
||||||
|
this._cpusSpan.innerText = cpus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMemory(memory) {
|
||||||
|
if (this._memorySpan) {
|
||||||
|
this._memorySpan.innerText = memory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateImage(image) {
|
||||||
|
if (this._imageSpan) {
|
||||||
|
this._imageSpan.innerText = image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateName(name) {
|
||||||
|
if (this._nameSpan) {
|
||||||
|
this._nameSpan.innerText = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._dialog.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder for container operations
|
||||||
|
async startContainer() {
|
||||||
|
if (!this._channelUid) return;
|
||||||
|
if (window.app?.rpc?.startContainer) {
|
||||||
|
await window.app.rpc.startContainer(this._channelUid);
|
||||||
|
await this.openWithStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopContainer() {
|
||||||
|
if (!this._channelUid) return;
|
||||||
|
if (window.app?.rpc?.stopContainer) {
|
||||||
|
await window.app.rpc.stopContainer(this._channelUid);
|
||||||
|
await this.openWithStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async restartContainer() {
|
||||||
|
if (!this._channelUid) return;
|
||||||
|
if (window.app?.rpc?.restartContainer) {
|
||||||
|
await window.app.rpc.restartContainer(this._channelUid);
|
||||||
|
await this.openWithStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('container-dialog', ContainerDialogComponent);
|
||||||
|
</script>
|
||||||
|
<container-dialog id="container-dialog" channel="{{ channel.uid.value }}"></container-dialog>
|
||||||
|
<script>
|
||||||
|
const containerDialog = document.getElementById('container-dialog');
|
||||||
|
</script>
|
@ -29,6 +29,7 @@
|
|||||||
</message-list>
|
</message-list>
|
||||||
<chat-input live-type="true" channel="{{ channel.uid.value }}"></chat-input>
|
<chat-input live-type="true" channel="{{ channel.uid.value }}"></chat-input>
|
||||||
</section>
|
</section>
|
||||||
|
{% include "dialog_container.html" %}
|
||||||
{% include "dialog_help.html" %}
|
{% include "dialog_help.html" %}
|
||||||
{% include "dialog_online.html" %}
|
{% include "dialog_online.html" %}
|
||||||
<script type="module">
|
<script type="module">
|
||||||
@ -47,7 +48,8 @@ chatInputField.autoCompletions = {
|
|||||||
"/online": showOnline,
|
"/online": showOnline,
|
||||||
"/clear": () => { messagesContainer.innerHTML = ''; },
|
"/clear": () => { messagesContainer.innerHTML = ''; },
|
||||||
"/live": () => { chatInputField.liveType = !chatInputField.liveType; },
|
"/live": () => { chatInputField.liveType = !chatInputField.liveType; },
|
||||||
"/help": showHelp
|
"/help": showHelp,
|
||||||
|
"/container": () =>{ containerDialog.openWithStatus()}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Throttle utility ---
|
// --- Throttle utility ---
|
||||||
|
@ -202,6 +202,35 @@ class RPCView(BaseView):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return channels
|
return channels
|
||||||
|
|
||||||
|
async def get_container(self, channel_uid):
|
||||||
|
self._require_login()
|
||||||
|
channel_member = await self.services.channel_member.get(
|
||||||
|
channel_uid=channel_uid, user_uid=self.user_uid
|
||||||
|
)
|
||||||
|
if not channel_member:
|
||||||
|
raise Exception("Not allowed")
|
||||||
|
container = await self.services.container.get(channel_uid)
|
||||||
|
result = None
|
||||||
|
if container:
|
||||||
|
result = {
|
||||||
|
"name": await self.services.container.get_container_name(channel_uid),
|
||||||
|
"cpus": container["deploy"]["resources"]["limits"]["cpus"],
|
||||||
|
"memory": container["deploy"]["resources"]["limits"]["memory"],
|
||||||
|
"image": container["image"],
|
||||||
|
"volumes": [],
|
||||||
|
"status": container["status"]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def get_container_status(self, channel_uid):
|
||||||
|
self._require_login()
|
||||||
|
channel_member = await self.services.channel_member.get(
|
||||||
|
channel_uid=channel_uid, user_uid=self.user_uid
|
||||||
|
)
|
||||||
|
if not channel_member:
|
||||||
|
raise Exception("Not allowed")
|
||||||
|
return await self.services.container.get_status(channel_uid)
|
||||||
|
|
||||||
async def finalize_message(self, message_uid):
|
async def finalize_message(self, message_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
|
Loading…
Reference in New Issue
Block a user