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