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): | ||||
|         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( | ||||
|         self, | ||||
|         channel_uid, | ||||
| @ -61,11 +67,3 @@ class ContainerService(BaseService): | ||||
|             return model | ||||
|         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 { | ||||
|   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 { | ||||
|   display: flex; | ||||
| @ -634,3 +658,7 @@ dialog .dialog-button.secondary:hover { | ||||
|   text-decoration: none; | ||||
|   margin-top: 10px; | ||||
| } | ||||
| 
 | ||||
| th { | ||||
|     min-width: 100px; | ||||
| } | ||||
|  | ||||
| @ -2,6 +2,10 @@ import copy | ||||
| 
 | ||||
| import yaml | ||||
| 
 | ||||
| import yaml | ||||
| import copy | ||||
| import asyncio | ||||
| import subprocess | ||||
| 
 | ||||
| class ComposeFileManager: | ||||
|     def __init__(self, compose_path="docker-compose.yml"): | ||||
| @ -58,8 +62,14 @@ class ComposeFileManager: | ||||
|             del self.compose["services"][name] | ||||
|             self._save() | ||||
| 
 | ||||
|     def get_instance(self, name): | ||||
|         return self.compose.get("services", {}).get(name) | ||||
|     async def get_instance(self, 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): | ||||
|         orig = self.get_instance(name) | ||||
| @ -78,6 +88,18 @@ class ComposeFileManager: | ||||
|         self.compose["services"][name] = service | ||||
|         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. | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										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> | ||||
|     <chat-input live-type="true" channel="{{ channel.uid.value }}"></chat-input> | ||||
| </section> | ||||
| {% include "dialog_container.html" %} | ||||
| {% include "dialog_help.html" %} | ||||
| {% include "dialog_online.html" %} | ||||
| <script type="module"> | ||||
| @ -47,7 +48,8 @@ chatInputField.autoCompletions = { | ||||
|     "/online": showOnline, | ||||
|     "/clear": () => { messagesContainer.innerHTML = ''; }, | ||||
|     "/live": () => { chatInputField.liveType = !chatInputField.liveType; }, | ||||
|     "/help": showHelp | ||||
|     "/help": showHelp, | ||||
|     "/container": () =>{ containerDialog.openWithStatus()} | ||||
| }; | ||||
| 
 | ||||
| // --- Throttle utility --- | ||||
|  | ||||
| @ -203,6 +203,35 @@ class RPCView(BaseView): | ||||
|                 ) | ||||
|             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): | ||||
|             self._require_login() | ||||
|             message = await self.services.channel_message.get(message_uid) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user