Compare commits
	
		
			26 Commits
		
	
	
		
			feat/ui-up
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5b2110d6e0 | |||
| a62be9b5e7 | |||
| 2b392d76d4 | |||
| 981555bcf3 | |||
| 57750c56d1 | |||
| 84f436817b | |||
| d24e42ec8e | |||
| 7d768127a6 | |||
| e9b98ae716 | |||
| bed711f46c | |||
| ed4ea2eb62 | |||
| 6c87655ee6 | |||
| 33ac6f9ad6 | |||
| 861d18484f | |||
| 50963324f3 | |||
| 4e1a2ba20b | |||
| 7030b95dc5 | |||
| f86fae86ef | |||
| 5d68e355f8 | |||
| 89396ebac9 | |||
| 1aef5afe18 | |||
|   | aa0c15bd7a | ||
|   | 1d9c4e0164 | ||
|   | b9e87bd0ad | ||
|   | e3845dbc8f | ||
|   | 160d2faab6 | 
							
								
								
									
										12
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | # 'dev' or whatever for dev environment, otherwise 'prod' to enable production environment | ||||||
|  | ENV=dev | ||||||
|  | 
 | ||||||
|  | # used only on production environment | ||||||
|  | HOST= | ||||||
|  | 
 | ||||||
|  | POSTGRES_DB=mumui | ||||||
|  | POSTGRES_USER=mumui | ||||||
|  | POSTGRES_PASSWORD=admin | ||||||
|  | 
 | ||||||
|  | # used at installation | ||||||
|  | ADMIN_PASSWORD=admin | ||||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -4,4 +4,6 @@ db.sqlite3 | |||||||
| venv | venv | ||||||
| /static | /static | ||||||
| *.log | *.log | ||||||
| *.pid | *.pid | ||||||
|  | docker-compose.override.yml | ||||||
|  | .env | ||||||
							
								
								
									
										14
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,12 +1,8 @@ | |||||||
| # TODO(rmanach): add a pyproject.toml
 |  | ||||||
| 
 |  | ||||||
| format: | format: | ||||||
| 	black deployment/*.py | 	@./venv/bin/black mumui/*.py deployment/*.py | ||||||
| 	black mumui/*.py |  | ||||||
| 
 | 
 | ||||||
| lint: | lint: | ||||||
| 	ruff deployment/*.py | 	@./venv/bin/ruff . | ||||||
| 	ruff mumui/*.py |  | ||||||
| 
 | 
 | ||||||
| dev: | dev: | ||||||
| 	rm -rf venv | 	rm -rf venv | ||||||
| @ -18,10 +14,10 @@ django: | |||||||
| 	docker build . -t mumui:local | 	docker build . -t mumui:local | ||||||
| 
 | 
 | ||||||
| pushpin-local: | pushpin-local: | ||||||
| 	cd pushpin && docker build . -t pushpin:local | 	cd pushpin && docker build . -t pushpin:mumui | ||||||
| 
 | 
 | ||||||
| nginx-local: | nginx-local: | ||||||
| 	cd nginx && docker build . -t nginx:local | 	cd nginx && docker build . -t nginx:mumui | ||||||
| 
 | 
 | ||||||
| build: | build: | ||||||
| 	$(MAKE) pushpin-local | 	$(MAKE) pushpin-local | ||||||
| @ -32,4 +28,4 @@ run: | |||||||
| 	docker compose up | 	docker compose up | ||||||
| 
 | 
 | ||||||
| stop: | stop: | ||||||
| 	docker compose down | 	docker compose down | ||||||
|  | |||||||
| @ -6,6 +6,12 @@ | |||||||
| 
 | 
 | ||||||
| Well, i had to choose a project name and i have no idea... I was listening this [sh*t](https://www.youtube.com/watch?v=8ZawzGgwIbQ) and then a sudden flash came to me: **mumu-i** (**i** for interface, to be more professional). | Well, i had to choose a project name and i have no idea... I was listening this [sh*t](https://www.youtube.com/watch?v=8ZawzGgwIbQ) and then a sudden flash came to me: **mumu-i** (**i** for interface, to be more professional). | ||||||
| 
 | 
 | ||||||
|  | You can test the application here: [mumui.thegux.fr](https://mumui.thegux.fr). | ||||||
|  | * username: **demo** | ||||||
|  | * password: **demo1234** | ||||||
|  | 
 | ||||||
|  | Enjoy ! 😛 | ||||||
|  | 
 | ||||||
| ## Build | ## Build | ||||||
| Some images to build: | Some images to build: | ||||||
| 
 | 
 | ||||||
| @ -24,6 +30,4 @@ make run | |||||||
| 
 | 
 | ||||||
| Connect to [localhost:8080](http://localhost:8080), login as **admin** with password **admin** and start to play with deployments. | Connect to [localhost:8080](http://localhost:8080), login as **admin** with password **admin** and start to play with deployments. | ||||||
| 
 | 
 | ||||||
| Enjoy ! 😛 |  | ||||||
| 
 |  | ||||||
| PS: more explanations about the project and the stack are coming soon. I promise. 😘 | PS: more explanations about the project and the stack are coming soon. I promise. 😘 | ||||||
| @ -1,3 +1,15 @@ | |||||||
| # from django.contrib import admin | from django.contrib import admin | ||||||
| 
 | 
 | ||||||
| # Register your models here. | from deployment.models import Deployment, DeploymentUser | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DeploymentUserAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DeploymentAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | admin.site.register(Deployment, DeploymentAdmin) | ||||||
|  | admin.site.register(DeploymentUser, DeploymentUserAdmin) | ||||||
|  | |||||||
| @ -4,3 +4,8 @@ from django.apps import AppConfig | |||||||
| class DeploymentConfig(AppConfig): | class DeploymentConfig(AppConfig): | ||||||
|     default_auto_field = "django.db.models.BigAutoField" |     default_auto_field = "django.db.models.BigAutoField" | ||||||
|     name = "deployment" |     name = "deployment" | ||||||
|  | 
 | ||||||
|  |     def ready(self) -> None: | ||||||
|  |         import deployment.signals  # noqa | ||||||
|  | 
 | ||||||
|  |         return super().ready() | ||||||
|  | |||||||
| @ -1,6 +0,0 @@ | |||||||
| from django_eventstream.channelmanager import DefaultChannelManager |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class DeploymentChannelManager(DefaultChannelManager): |  | ||||||
|     def can_read_channel(self, user, channel): |  | ||||||
|         return user is not None and user.is_authenticated |  | ||||||
							
								
								
									
										51
									
								
								deployment/channels.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								deployment/channels.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | from django.contrib.auth.models import User | ||||||
|  | from django_eventstream import send_event | ||||||
|  | from django_eventstream.channelmanager import DefaultChannelManager | ||||||
|  | 
 | ||||||
|  | from deployment.models import Deployment | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def parse_channel(channel: str) -> tuple[str] | None: | ||||||
|  |     parts = channel.lstrip("deployment_").split("_") | ||||||
|  |     if len_part := len(parts): | ||||||
|  |         if len_part == 1: | ||||||
|  |             return (int(parts[0]), "") | ||||||
|  |         if len_part == 2: | ||||||
|  |             return (int(parts[0]), parts[1]) | ||||||
|  |     return | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DeploymentChannelManager(DefaultChannelManager): | ||||||
|  |     def can_read_channel(self, user: User, channel: str): | ||||||
|  |         if not user.has_perm("django_eventstream.view_event"): | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |         match parse_channel(channel): | ||||||
|  |             case (user_id, ""): | ||||||
|  |                 return user_id == user.id or user.is_superuser | ||||||
|  |             case (user_id, _): | ||||||
|  |                 # TODO(rmanach): check if the deployment belongs to the user | ||||||
|  |                 return user_id == user.id or user.is_superuser | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Event: | ||||||
|  |     @staticmethod | ||||||
|  |     def send_details(deployment: Deployment, progress: int): | ||||||
|  |         send_event( | ||||||
|  |             f"deployment_{deployment.user.id}_{deployment.id}", | ||||||
|  |             "message", | ||||||
|  |             { | ||||||
|  |                 "id": deployment.id, | ||||||
|  |                 "status": deployment.status, | ||||||
|  |                 "progress": progress, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def send(deployment: Deployment): | ||||||
|  |         send_event( | ||||||
|  |             f"deployment_{deployment.user.id}", | ||||||
|  |             "message", | ||||||
|  |             {"id": deployment.id, "status": deployment.status}, | ||||||
|  |         ) | ||||||
| @ -1,4 +1,4 @@ | |||||||
| # Generated by Django 4.2.5 on 2023-09-20 17:23 | # Generated by Django 4.2.5 on 2023-09-22 14:39 | ||||||
| 
 | 
 | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
| @ -46,6 +46,8 @@ class Migration(migrations.Migration): | |||||||
|                 ), |                 ), | ||||||
|                 ("created_at", models.DateTimeField(auto_now_add=True)), |                 ("created_at", models.DateTimeField(auto_now_add=True)), | ||||||
|                 ("updated_at", models.DateTimeField(auto_now=True)), |                 ("updated_at", models.DateTimeField(auto_now=True)), | ||||||
|  |                 ("error", models.TextField(null=True)), | ||||||
|  |                 ("task_id", models.UUIDField(null=True)), | ||||||
|                 ( |                 ( | ||||||
|                     "user", |                     "user", | ||||||
|                     models.ForeignKey( |                     models.ForeignKey( | ||||||
|  | |||||||
							
								
								
									
										25
									
								
								deployment/migrations/0002_deploymentuser.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								deployment/migrations/0002_deploymentuser.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | # Generated by Django 4.2.5 on 2023-09-23 14:21 | ||||||
|  | 
 | ||||||
|  | from django.conf import settings | ||||||
|  | import django.core.validators | ||||||
|  | from django.db import migrations, models | ||||||
|  | import django.db.models.deletion | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|  |         ('deployment', '0001_initial'), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='DeploymentUser', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('credits', models.SmallIntegerField(default=1, help_text='number of deployment allowed (-1 infinite)', validators=[django.core.validators.MinValueValidator(-1, message='Value must be greater than or equal to -1')])), | ||||||
|  |                 ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -1,8 +1,9 @@ | |||||||
| from enum import Enum | from enum import Enum | ||||||
| from datetime import datetime | from typing import Any | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
|  | from django.core.validators import MinValueValidator | ||||||
| from django.db import models | from django.db import models | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -28,6 +29,21 @@ class Status(Enum): | |||||||
|         return [(s.name, s.value) for s in cls] |         return [(s.name, s.value) for s in cls] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class DeploymentUser(models.Model): | ||||||
|  |     user = models.OneToOneField(User, on_delete=models.CASCADE) | ||||||
|  |     credits = models.SmallIntegerField( | ||||||
|  |         null=False, | ||||||
|  |         default=1, | ||||||
|  |         help_text="number of deployment allowed (-1 infinite)", | ||||||
|  |         validators=[ | ||||||
|  |             MinValueValidator(-1, message="Value must be greater than or equal to -1"), | ||||||
|  |         ], | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return f"{self.user.username} - {self.credits}" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class Deployment(models.Model): | class Deployment(models.Model): | ||||||
|     id = models.UUIDField(primary_key=True) |     id = models.UUIDField(primary_key=True) | ||||||
|     name = models.TextField(null=False, blank=False) |     name = models.TextField(null=False, blank=False) | ||||||
| @ -39,7 +55,13 @@ class Deployment(models.Model): | |||||||
|     ) |     ) | ||||||
|     created_at = models.DateTimeField(auto_now_add=True) |     created_at = models.DateTimeField(auto_now_add=True) | ||||||
|     updated_at = models.DateTimeField(auto_now=True) |     updated_at = models.DateTimeField(auto_now=True) | ||||||
|  |     error = models.TextField(null=True, blank=False) | ||||||
|  |     task_id = models.UUIDField(null=True) | ||||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) |     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
| 
 | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return f"{self.name} | {self.type} | {self.status}" |         return f"{self.name} | {self.type} | {self.status}" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, *args: Any, **kwargs: Any): | ||||||
|  |         self.progress = None | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  | |||||||
							
								
								
									
										27
									
								
								deployment/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								deployment/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | from django.contrib.auth.models import User, Permission | ||||||
|  | from django.db import transaction | ||||||
|  | from django.db.models.signals import post_save | ||||||
|  | from django.dispatch import receiver | ||||||
|  | 
 | ||||||
|  | from deployment.models import DeploymentUser | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @receiver(post_save, sender=User) | ||||||
|  | def user_created_callback(sender, instance: User, created, **kwargs): | ||||||
|  |     if created: | ||||||
|  |         try: | ||||||
|  |             with transaction.atomic(): | ||||||
|  |                 if not instance.is_superuser: | ||||||
|  |                     permission = Permission.objects.get( | ||||||
|  |                         codename="view_event", | ||||||
|  |                         content_type__app_label="django_eventstream", | ||||||
|  |                     ) | ||||||
|  |                     instance.user_permissions.add(permission) | ||||||
|  |                     permission.save() | ||||||
|  | 
 | ||||||
|  |                 user = DeploymentUser( | ||||||
|  |                     user=instance, credits=-1 if instance.is_superuser else 3 | ||||||
|  |                 ) | ||||||
|  |                 user.save() | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"deployment user creation failed: {e}") | ||||||
| @ -1,7 +1,8 @@ | |||||||
| var start = function (url) { | var start = function (url) { | ||||||
| 
 |  | ||||||
|     var es = new ReconnectingEventSource(url); |     var es = new ReconnectingEventSource(url); | ||||||
| 
 | 
 | ||||||
|  |     console.log("url: " + url); | ||||||
|  | 
 | ||||||
|     es.onopen = function () { |     es.onopen = function () { | ||||||
|         console.log('connected'); |         console.log('connected'); | ||||||
|     }; |     }; | ||||||
| @ -29,14 +30,34 @@ var start = function (url) { | |||||||
|             status.innerHTML = message.status; |             status.innerHTML = message.status; | ||||||
| 
 | 
 | ||||||
|             var button = tr.querySelector("th[name='deploy']"); |             var button = tr.querySelector("th[name='deploy']"); | ||||||
|  |             if (message.status == "RUNNING") { | ||||||
|  |                 var innerBtn = document.createElement("button"); | ||||||
|  |                 innerBtn.setAttribute("disabled", ""); | ||||||
|  |                 innerBtn.setAttribute("type", "button"); | ||||||
|  |                 innerBtn.className = "btn btn-primary btn-sm" | ||||||
|  | 
 | ||||||
|  |                 var innerSpan = document.createElement("span"); | ||||||
|  |                 innerSpan.setAttribute("role", "status"); | ||||||
|  |                 innerSpan.innerHTML = " Deploying..."; | ||||||
|  | 
 | ||||||
|  |                 var innerSpinner = document.createElement("span"); | ||||||
|  |                 innerSpinner.className = "spinner-border spinner-border-sm"; | ||||||
|  |                 innerSpinner.setAttribute("aria-hidden", "true"); | ||||||
|  |                  | ||||||
|  |                 innerBtn.appendChild(innerSpinner); | ||||||
|  |                 innerBtn.appendChild(innerSpan); | ||||||
|  | 
 | ||||||
|  |                 button.innerHTML = ""; | ||||||
|  |                 button.appendChild(innerBtn); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             if (message.status == "SUCCESS") { |             if (message.status == "SUCCESS") { | ||||||
|                 button.innerHTML = ""; |                 button.innerHTML = ""; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (message.status == "FAILED") { |             if (message.status == "FAILED") { | ||||||
|                     window.location.reload(); |                 window.location.reload(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     }, false); |     }, false); | ||||||
| }; | }; | ||||||
							
								
								
									
										43
									
								
								deployment/static/deployment/js/event_source_details.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								deployment/static/deployment/js/event_source_details.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | var start = function (url) { | ||||||
|  |     var es = new ReconnectingEventSource(url); | ||||||
|  | 
 | ||||||
|  |     console.log("url: " + url); | ||||||
|  | 
 | ||||||
|  |     es.onopen = function () { | ||||||
|  |         console.log('connected'); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     es.addEventListener('stream-error', function (e) { | ||||||
|  |         es.close(); | ||||||
|  |         message = JSON.parse(e.data); | ||||||
|  |         console.log('stream error: ' + message.condition + ': ' + message.text); | ||||||
|  |     }, false); | ||||||
|  | 
 | ||||||
|  |     es.onerror = function (e) { | ||||||
|  |         console.log('connection error'); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     es.addEventListener('message', function (e) { | ||||||
|  |         message = JSON.parse(e.data); | ||||||
|  |         console.log("id: " + message.id); | ||||||
|  |         console.log("status: " + message.status); | ||||||
|  |         console.log("progress: " + message.progress); | ||||||
|  | 
 | ||||||
|  |         var status = document.getElementById("status"); | ||||||
|  |         status.setAttribute("value", message.status) | ||||||
|  | 
 | ||||||
|  |         var progress = document.getElementById("deployment-progress"); | ||||||
|  |         // no progress in `PENDING` state
 | ||||||
|  |         if (progress !== undefined) { | ||||||
|  |             progress.style["width"] = message.progress+"%"; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (message.status == "SUCCESS") { | ||||||
|  |             setTimeout(() => window.location.reload(), 1000); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (message.status == "FAILED") { | ||||||
|  |             window.location.reload(); | ||||||
|  |         } | ||||||
|  |     }, false); | ||||||
|  | }; | ||||||
							
								
								
									
										
											BIN
										
									
								
								deployment/static/deployment/png/flash-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								deployment/static/deployment/png/flash-icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 27 KiB | 
| @ -3,30 +3,100 @@ import random | |||||||
| from uuid import UUID | from uuid import UUID | ||||||
| 
 | 
 | ||||||
| from celery import shared_task | from celery import shared_task | ||||||
| from django_eventstream import send_event | from celery.contrib.abortable import AbortableTask | ||||||
| 
 | 
 | ||||||
|  | from deployment.channels import Event | ||||||
| from deployment.models import Deployment, Type, Status | from deployment.models import Deployment, Type, Status | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @shared_task | class DeploymentTask(AbortableTask): | ||||||
| def deploy(deployment_id: UUID): |     def try_exec(self, stop: int): | ||||||
|     deploy = Deployment.objects.get(id=deployment_id) |         if stop == 0: | ||||||
|  |             stop = 1 | ||||||
| 
 | 
 | ||||||
|     deploy.status = Status.RUNNING.name |         for i in range(1, stop): | ||||||
|     deploy.save() |             time.sleep(5 * stop - 2 * i) | ||||||
| 
 | 
 | ||||||
|     match deploy.type: |             if random.randint(0, 10) == 10: | ||||||
|         case Type.SLIM.name: |                 raise Exception("unable to perform the deployment") | ||||||
|             time.sleep(10) |  | ||||||
|         case Type.MEDIUM.name: |  | ||||||
|             time.sleep(60) |  | ||||||
|         case Type.LARGE.name: |  | ||||||
|             time.sleep(120) |  | ||||||
| 
 | 
 | ||||||
|     deploy.status = ( |             yield i | ||||||
|         Status.FAILED.name if random.randint(0, 10) % 2 != 0 else Status.SUCCESS.name |  | ||||||
|     ) |  | ||||||
|     deploy.save() |  | ||||||
| 
 | 
 | ||||||
|     send_event("deployment", "message", {"id": deploy.id, "status": deploy.status}) |     def run_slim(self): | ||||||
|     print("event sent") |         progress = 95 | ||||||
|  |         self.update_state(meta={"progress": progress}) | ||||||
|  | 
 | ||||||
|  |         time.sleep(5) | ||||||
|  |         status = Status.SUCCESS.name | ||||||
|  |         progress = 100 | ||||||
|  |         self.update_state(meta={"progress": progress}) | ||||||
|  | 
 | ||||||
|  |         self.deploy.status = status | ||||||
|  |         Event.send_details(self.deploy, progress) | ||||||
|  | 
 | ||||||
|  |     def run_medium(self): | ||||||
|  |         progress = 62 | ||||||
|  |         self.update_state(meta={"progress": progress}) | ||||||
|  | 
 | ||||||
|  |         time.sleep(10) | ||||||
|  |         if random.randint(0, 10) % 2 != 0: | ||||||
|  |             status = Status.FAILED.name | ||||||
|  |             self.deploy.error = "arg.. no chance" | ||||||
|  |         else: | ||||||
|  |             status = Status.SUCCESS.name | ||||||
|  |             progress = 100 | ||||||
|  |             self.update_state(meta={"progress": progress}) | ||||||
|  | 
 | ||||||
|  |         self.deploy.status = status | ||||||
|  |         Event.send_details(self.deploy, progress) | ||||||
|  | 
 | ||||||
|  |     def run_large(self): | ||||||
|  |         progress = 0 | ||||||
|  |         try: | ||||||
|  |             for i in self.try_exec(6): | ||||||
|  |                 progress = i * 20 | ||||||
|  |                 self.update_state(meta={"progress": progress}) | ||||||
|  |                 if i != 5: | ||||||
|  |                     Event.send_details(self.deploy, progress) | ||||||
|  |         except Exception as e: | ||||||
|  |             self.deploy.status = Status.FAILED.name | ||||||
|  |             self.deploy.error = e | ||||||
|  |         else: | ||||||
|  |             self.deploy.status = Status.SUCCESS.name | ||||||
|  | 
 | ||||||
|  |         Event.send_details(self.deploy, progress) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def progress(self): | ||||||
|  |         return self.request.get("meta", {}).get("progress", 0) | ||||||
|  | 
 | ||||||
|  |     def launch(self, deployment_id: UUID): | ||||||
|  |         progress = 0 | ||||||
|  |         self.update_state(meta={"progress": progress}) | ||||||
|  | 
 | ||||||
|  |         self.deploy = Deployment.objects.get(id=deployment_id) | ||||||
|  | 
 | ||||||
|  |         self.deploy.task_id = self.request.id | ||||||
|  |         self.deploy.status = Status.RUNNING.name | ||||||
|  |         self.deploy.save() | ||||||
|  | 
 | ||||||
|  |         Event.send(self.deploy) | ||||||
|  |         Event.send_details(self.deploy, 0) | ||||||
|  | 
 | ||||||
|  |         match self.deploy.type: | ||||||
|  |             case Type.SLIM.name: | ||||||
|  |                 self.run_slim() | ||||||
|  |             case Type.MEDIUM.name: | ||||||
|  |                 self.run_medium() | ||||||
|  |             case Type.LARGE.name: | ||||||
|  |                 self.run_large() | ||||||
|  | 
 | ||||||
|  |         self.deploy.task_id = None | ||||||
|  |         self.deploy.save() | ||||||
|  | 
 | ||||||
|  |         Event.send(self.deploy) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @shared_task(base=DeploymentTask, bind=True, ignore_result=True) | ||||||
|  | def deploy(self, deployment_id: UUID): | ||||||
|  |     self.launch(deployment_id) | ||||||
|  | |||||||
| @ -1,17 +1,23 @@ | |||||||
| import django_eventstream | import django_eventstream | ||||||
| 
 | 
 | ||||||
| from django.urls import path, include | from django.urls import path, include | ||||||
| from deployment.views import index, create, details, deploy | from deployment.views import index, create, details, deploy, abort | ||||||
| 
 | 
 | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path("", index, name="deployment"), |     path("", index, name="deployments"), | ||||||
|     path("create", create, name="deployment-create"), |     path("create", create, name="deployment-create"), | ||||||
|     path("<uuid:deployment_id>", details, name="deployment-details"), |     path("<uuid:deployment_id>", details, name="deployment-details"), | ||||||
|     path("<uuid:deployment_id>/deploy", deploy, name="deployment-launch"), |     path("<uuid:deployment_id>/deploy", deploy, name="deployment-launch"), | ||||||
|  |     path("<uuid:deployment_id>/abort", abort, name="deployment-abort"), | ||||||
|     path( |     path( | ||||||
|         "events/", |         "events/<user_id>/", | ||||||
|         include(django_eventstream.urls), |         include(django_eventstream.urls), | ||||||
|         {"channels": ["deployment"]}, |         {"format-channels": ["deployment_{user_id}"]}, | ||||||
|         name="deployment-events", |         name="deployments-events", | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         "events/<user_id>/<deployment_id>/", | ||||||
|  |         include(django_eventstream.urls), | ||||||
|  |         {"format-channels": ["deployment_{user_id}_{deployment_id}"]}, | ||||||
|     ), |     ), | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -1,18 +1,39 @@ | |||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
| 
 | 
 | ||||||
|  | from celery.result import AsyncResult | ||||||
|  | from celery.contrib.abortable import AbortableAsyncResult | ||||||
|  | from django.contrib.auth.models import User | ||||||
|  | from django.core.exceptions import ObjectDoesNotExist | ||||||
| from django.core.paginator import Paginator | from django.core.paginator import Paginator | ||||||
| from django.http import ( | from django.http import ( | ||||||
|     HttpResponseRedirect, |     HttpResponseRedirect, | ||||||
|     HttpResponseServerError, |     HttpResponseServerError, | ||||||
|     HttpResponseBadRequest, |     HttpResponseBadRequest, | ||||||
|  |     HttpResponseForbidden, | ||||||
| ) | ) | ||||||
| from django.shortcuts import render, get_object_or_404 | from django.shortcuts import render, get_object_or_404 | ||||||
| 
 | 
 | ||||||
|  | from deployment.channels import Event | ||||||
| from deployment.forms import DeploymentForm | from deployment.forms import DeploymentForm | ||||||
| from deployment.models import Deployment, Status | from deployment.models import Deployment, Status | ||||||
| from deployment.tasks import deploy as launch_deploy | from deployment.tasks import deploy as launch_deploy | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def check_user_credits(user: User) -> bool: | ||||||
|  |     if not user.is_superuser: | ||||||
|  |         try: | ||||||
|  |             dep_user = user.deploymentuser | ||||||
|  |         except (ObjectDoesNotExist, AttributeError): | ||||||
|  |             return False | ||||||
|  |         else: | ||||||
|  |             if dep_user.credits < 0: | ||||||
|  |                 return True | ||||||
|  | 
 | ||||||
|  |             deployments = Deployment.objects.filter(user=user) | ||||||
|  |             return dep_user.credits > len(deployments) | ||||||
|  |     return True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def index(request): | def index(request): | ||||||
|     deployments = Deployment.objects.filter(user=request.user.id).order_by( |     deployments = Deployment.objects.filter(user=request.user.id).order_by( | ||||||
|         "-created_at" |         "-created_at" | ||||||
| @ -27,8 +48,9 @@ def index(request): | |||||||
|         "deployment/board.html", |         "deployment/board.html", | ||||||
|         { |         { | ||||||
|             "page_obj": page_obj, |             "page_obj": page_obj, | ||||||
|             "range_pages": [i + 1 for i in range(page_obj.paginator.num_pages)], |             "range_pages": (i + 1 for i in range(page_obj.paginator.num_pages)), | ||||||
|             "url": "events/", |             "url": f"events/{request.user.id}/", | ||||||
|  |             "can_create": check_user_credits(request.user), | ||||||
|         }, |         }, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| @ -37,40 +59,90 @@ def deploy(request, deployment_id): | |||||||
|     deployment = get_object_or_404(Deployment, id=deployment_id) |     deployment = get_object_or_404(Deployment, id=deployment_id) | ||||||
| 
 | 
 | ||||||
|     if request.method == "POST": |     if request.method == "POST": | ||||||
|  |         if deployment.status not in (Status.READY.name, Status.FAILED.name): | ||||||
|  |             return HttpResponseBadRequest("deployment is undeployable") | ||||||
|  | 
 | ||||||
|  |         # override previous errors | ||||||
|  |         if deployment.error: | ||||||
|  |             deployment.error = None | ||||||
|  | 
 | ||||||
|         deployment.status = Status.PENDING.name |         deployment.status = Status.PENDING.name | ||||||
|         deployment.save() |         deployment.save() | ||||||
|         launch_deploy.delay(deployment_id) |         launch_deploy.delay(deployment_id) | ||||||
| 
 | 
 | ||||||
|     if page := request.GET.get("page", ""): |     if page := request.GET.get("page", ""): | ||||||
|         return HttpResponseRedirect(f"/deployment?page={page}") |         return HttpResponseRedirect(f"/deployments?page={page}") | ||||||
| 
 | 
 | ||||||
|     return HttpResponseRedirect("/deployment") |     return HttpResponseRedirect("/deployments") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def abort(request, deployment_id): | ||||||
|  |     deployment = get_object_or_404(Deployment, id=deployment_id) | ||||||
|  | 
 | ||||||
|  |     if request.method == "POST": | ||||||
|  |         if deployment.status not in (Status.RUNNING.name, Status.PENDING.name): | ||||||
|  |             return HttpResponseBadRequest("deployment is unabortable") | ||||||
|  | 
 | ||||||
|  |         res = AbortableAsyncResult(str(deployment.task_id)) | ||||||
|  |         progress = res.info.get("progress", 0) | ||||||
|  |         res.revoke(terminate=True) | ||||||
|  | 
 | ||||||
|  |         deployment.status = Status.FAILED.name | ||||||
|  |         deployment.error = f"aborted by {request.user}" | ||||||
|  |         deployment.task_id = None | ||||||
|  |         deployment.save() | ||||||
|  | 
 | ||||||
|  |         Event.send_details(deployment, progress) | ||||||
|  |         Event.send(deployment) | ||||||
|  | 
 | ||||||
|  |     return HttpResponseRedirect(f"/deployments/{deployment.id}") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def details(request, deployment_id): | def details(request, deployment_id): | ||||||
|     deployment = get_object_or_404(Deployment, id=deployment_id) |     deployment = get_object_or_404(Deployment, id=deployment_id) | ||||||
|  |     if deployment.status == Status.RUNNING.name: | ||||||
|  |         # retrieve the progression task in Redis | ||||||
|  |         res = AsyncResult(str(deployment.task_id)) | ||||||
|  |         deployment.progress = res.info.get("progress", 0) | ||||||
| 
 | 
 | ||||||
|     if request.method == "POST": |     if request.method == "POST": | ||||||
|         if deployment.status == Status.RUNNING.name: |         if deployment.status == Status.RUNNING.name: | ||||||
|             return HttpResponseBadRequest("deployment is running") |             return HttpResponseBadRequest("deployment is running") | ||||||
|  | 
 | ||||||
|  |         if deployment.status == Status.PENDING.name: | ||||||
|  |             return HttpResponseBadRequest("deployment is pending") | ||||||
|  | 
 | ||||||
|         try: |         try: | ||||||
|             deployment.delete() |             deployment.delete() | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             return HttpResponseServerError(e) |             return HttpResponseServerError(e) | ||||||
|         return HttpResponseRedirect("/deployment") |         return HttpResponseRedirect("/deployments") | ||||||
| 
 | 
 | ||||||
|     return render(request, "deployment/details.html", {"deployment": deployment}) |     return render( | ||||||
|  |         request, | ||||||
|  |         "deployment/details.html", | ||||||
|  |         {"deployment": deployment, "url": f"events/{request.user.id}/{deployment.id}/"}, | ||||||
|  |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def create(request): | def create(request): | ||||||
|     if request.method == "POST": |     if request.method == "POST": | ||||||
|  |         if not check_user_credits(request.user): | ||||||
|  |             return HttpResponseForbidden( | ||||||
|  |                 "unable to launch deployment, contact administrator for support" | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|         form = DeploymentForm(request.POST) |         form = DeploymentForm(request.POST) | ||||||
|         if form.is_valid(): |         if not form.is_valid(): | ||||||
|             try: |             return HttpResponseBadRequest( | ||||||
|                 Deployment.objects.create( |                 f"deployment creation inputs are invalid: {form.errors}" | ||||||
|                     user=request.user, id=uuid4(), **form.cleaned_data |             ) | ||||||
|                 ) | 
 | ||||||
|             except Exception as e: |         try: | ||||||
|                 return HttpResponseServerError(e) |             Deployment.objects.create( | ||||||
|              |                 user=request.user, id=uuid4(), **form.cleaned_data | ||||||
|     return HttpResponseRedirect("/deployment") |             ) | ||||||
|  |         except Exception as e: | ||||||
|  |             return HttpResponseServerError(e) | ||||||
|  | 
 | ||||||
|  |     return HttpResponseRedirect("/deployments") | ||||||
|  | |||||||
| @ -2,15 +2,15 @@ version: '3' | |||||||
| services: | services: | ||||||
|   redis: |   redis: | ||||||
|     image: redis/redis-stack-server:latest |     image: redis/redis-stack-server:latest | ||||||
|     container_name: redis |     container_name: redis-mumui | ||||||
|     networks: |     networks: | ||||||
|       - mumui_network |       - mumui_network | ||||||
|     volumes: |     volumes: | ||||||
|       - redis_data:/data |       - redis_data:/data | ||||||
| 
 | 
 | ||||||
|   pushpin: |   pushpin: | ||||||
|     image: pushpin:local |     image: pushpin:mumui | ||||||
|     container_name: pushpin |     container_name: pushpin-mumui | ||||||
|     networks: |     networks: | ||||||
|       - mumui_network |       - mumui_network | ||||||
|     depends_on: |     depends_on: | ||||||
| @ -18,11 +18,9 @@ services: | |||||||
| 
 | 
 | ||||||
|   postgres: |   postgres: | ||||||
|     image: postgres:latest |     image: postgres:latest | ||||||
|     container_name: postgres |     container_name: postgres-mumui | ||||||
|     environment: |     env_file: | ||||||
|       POSTGRES_DB: mumui |       - .env | ||||||
|       POSTGRES_USER: test |  | ||||||
|       POSTGRES_PASSWORD: test |  | ||||||
|     networks: |     networks: | ||||||
|       - mumui_network |       - mumui_network | ||||||
|     volumes: |     volumes: | ||||||
| @ -31,6 +29,8 @@ services: | |||||||
|   mumui: |   mumui: | ||||||
|     image: mumui:local |     image: mumui:local | ||||||
|     container_name: mumui |     container_name: mumui | ||||||
|  |     env_file: | ||||||
|  |       - .env | ||||||
|     networks: |     networks: | ||||||
|       - mumui_network |       - mumui_network | ||||||
|     volumes: |     volumes: | ||||||
| @ -39,8 +39,8 @@ services: | |||||||
|       - postgres |       - postgres | ||||||
| 
 | 
 | ||||||
|   nginx: |   nginx: | ||||||
|     image: nginx:local |     image: nginx:mumui | ||||||
|     container_name: nginx |     container_name: nginx-mumui | ||||||
|     networks: |     networks: | ||||||
|       - mumui_network |       - mumui_network | ||||||
|     volumes: |     volumes: | ||||||
| @ -49,7 +49,6 @@ services: | |||||||
|       - "8080:8080" |       - "8080:8080" | ||||||
|     depends_on: |     depends_on: | ||||||
|       - mumui |       - mumui | ||||||
|    |  | ||||||
| 
 | 
 | ||||||
| networks: | networks: | ||||||
|   mumui_network: |   mumui_network: | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ from celery import Celery | |||||||
| 
 | 
 | ||||||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mumui.settings") | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mumui.settings") | ||||||
| 
 | 
 | ||||||
| app = Celery("mumui", broker="redis://redis:6379/0") | app = Celery("mumui", broker="redis://redis:6379/0", backend="redis://redis:6379/0") | ||||||
| 
 | 
 | ||||||
| app.config_from_object("django.conf:settings", namespace="CELERY") | app.config_from_object("django.conf:settings", namespace="CELERY") | ||||||
| app.autodiscover_tasks() | app.autodiscover_tasks() | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/ | |||||||
| For the full list of settings and their values, see | For the full list of settings and their values, see | ||||||
| https://docs.djangoproject.com/en/4.2/ref/settings/ | https://docs.djangoproject.com/en/4.2/ref/settings/ | ||||||
| """ | """ | ||||||
|  | import os | ||||||
| 
 | 
 | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| 
 | 
 | ||||||
| @ -22,10 +23,13 @@ BASE_DIR = Path(__file__).resolve().parent.parent | |||||||
| # SECURITY WARNING: keep the secret key used in production secret! | # SECURITY WARNING: keep the secret key used in production secret! | ||||||
| SECRET_KEY = "django-insecure-_c56%%c8%g%@5(3&thxi7ku2a&wst8lik*8@l0=#)ar)s86g36" | SECRET_KEY = "django-insecure-_c56%%c8%g%@5(3&thxi7ku2a&wst8lik*8@l0=#)ar)s86g36" | ||||||
| 
 | 
 | ||||||
| # SECURITY WARNING: don't run with debug turned on in production! | IS_PROD = os.getenv("ENV") == "prod" | ||||||
| DEBUG = True | HOST = "*" if not IS_PROD else os.getenv("HOST", "*") | ||||||
| 
 | 
 | ||||||
| ALLOWED_HOSTS = ["*"] | # SECURITY WARNING: don't run with debug turned on in production! | ||||||
|  | DEBUG = not IS_PROD | ||||||
|  | 
 | ||||||
|  | ALLOWED_HOSTS = ["*"] if not IS_PROD else [HOST.lstrip("https://")] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Application definition | # Application definition | ||||||
| @ -80,8 +84,8 @@ DATABASES = { | |||||||
|     "default": { |     "default": { | ||||||
|         "ENGINE": "django.db.backends.postgresql", |         "ENGINE": "django.db.backends.postgresql", | ||||||
|         "NAME": "mumui", |         "NAME": "mumui", | ||||||
|         "USER": "test", |         "USER": os.getenv("POSTGRES_USER", "test"), | ||||||
|         "PASSWORD": "test", |         "PASSWORD": os.getenv("POSTGRES_PASSWORD", "test"), | ||||||
|         "HOST": "postgres", |         "HOST": "postgres", | ||||||
|         "PORT": "5432", |         "PORT": "5432", | ||||||
|     } |     } | ||||||
| @ -133,6 +137,6 @@ LOGIN_REDIRECT_URL = "home" | |||||||
| LOGOUT_REDIRECT_URL = "home" | LOGOUT_REDIRECT_URL = "home" | ||||||
| 
 | 
 | ||||||
| GRIP_URL = "http://pushpin:5561" | GRIP_URL = "http://pushpin:5561" | ||||||
| EVENTSTREAM_CHANNELMANAGER_CLASS = "deployment.channel.DeploymentChannelManager" | EVENTSTREAM_CHANNELMANAGER_CLASS = "deployment.channels.DeploymentChannelManager" | ||||||
| 
 | 
 | ||||||
| CSRF_TRUSTED_ORIGINS = ["http://localhost:8080"] | CSRF_TRUSTED_ORIGINS = [HOST] if IS_PROD else ["http://localhost:8080"] | ||||||
|  | |||||||
| @ -21,6 +21,6 @@ from django.views.generic.base import TemplateView | |||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path("admin/", admin.site.urls), |     path("admin/", admin.site.urls), | ||||||
|     path("accounts/", include("django.contrib.auth.urls")), |     path("accounts/", include("django.contrib.auth.urls")), | ||||||
|     path("deployment/", include("deployment.urls")), |     path("deployments/", include("deployment.urls")), | ||||||
|     path("", TemplateView.as_view(template_name="home.html"), name="home"), |     path("", TemplateView.as_view(template_name="home.html"), name="home"), | ||||||
| ] | ] | ||||||
|  | |||||||
							
								
								
									
										19
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | [tool.black] | ||||||
|  | line-length = 88 | ||||||
|  | 
 | ||||||
|  | [tool.ruff] | ||||||
|  | select = ["E", "F"] | ||||||
|  | ignore = [] | ||||||
|  | exclude = [ | ||||||
|  |     ".git", | ||||||
|  |     ".ruff_cache", | ||||||
|  |     "venv", | ||||||
|  |     "settings.py", | ||||||
|  |     "deployment/migrations/*.py" | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | line-length = 88 | ||||||
|  | target-version = "py310" | ||||||
|  | 
 | ||||||
|  | [tool.ruff.mccabe] | ||||||
|  | max-complexity = 10 | ||||||
| @ -5,6 +5,6 @@ python manage.py makemigrations | |||||||
| python manage.py migrate | python manage.py migrate | ||||||
| python manage.py collectstatic --no-input | python manage.py collectstatic --no-input | ||||||
| 
 | 
 | ||||||
| DJANGO_SUPERUSER_PASSWORD=admin python manage.py createsuperuser --noinput --username admin --email admin@admin.fr | DJANGO_SUPERUSER_PASSWORD=${ADMIN_PASSWORD} python manage.py createsuperuser --noinput --username admin --email admin@admin.fr | ||||||
| 
 | 
 | ||||||
| supervisord -c /app/supervisord.conf | supervisord -c /app/supervisord.conf | ||||||
| @ -17,14 +17,14 @@ | |||||||
| {% block content %} | {% block content %} | ||||||
|     <div class="container-fluid"> |     <div class="container-fluid"> | ||||||
|         <div class="row justify-content-md-center"> |         <div class="row justify-content-md-center"> | ||||||
|             <div class="col-lg-8 col-md-8 col-sm-6"> |             <div class="col-lg-8 col-md-10 col-sm-12"> | ||||||
|                 {% if user.is_authenticated %} |                 {% if user.is_authenticated %} | ||||||
|                     <table class="table table-striped table-hover"> |                     <table class="table table-striped table-hover"> | ||||||
|                         <tr> |                         <tr> | ||||||
|                             <th scope="col-3">Name</th> |                             <th scope="col-3">Name</th> | ||||||
|                             <th scope="col-3">Type</th> |                             <th scope="col-3">Type</th> | ||||||
|                             <th scope="col-3">Status</th> |                             <th scope="col-3">Status</th> | ||||||
|                             <th class="col-2"></th> |                             <th class="col-1"></th> | ||||||
|                             <th class="col-2"></th> |                             <th class="col-2"></th> | ||||||
|                         </tr> |                         </tr> | ||||||
|                         {% for deployment in page_obj %} |                         {% for deployment in page_obj %} | ||||||
| @ -34,21 +34,27 @@ | |||||||
|                                 <th name="status">{{ deployment.status }}</th> |                                 <th name="status">{{ deployment.status }}</th> | ||||||
|                                 <th> |                                 <th> | ||||||
|                                     <a href="{% url 'deployment-details' deployment.id %}"> |                                     <a href="{% url 'deployment-details' deployment.id %}"> | ||||||
|                                         <button class="btn btn-primary">Details</button> |                                         <button class="btn btn-primary btn-sm">Details</button> | ||||||
|                                     </a> |                                     </a> | ||||||
|                                 </th> |                                 </th> | ||||||
|                                 {% if deployment.status == "FAILED" or deployment.status == "READY" %} |                                 {% if deployment.status == "FAILED" or deployment.status == "READY" %} | ||||||
|                                     <th name="deploy"> |                                     <th name="deploy"> | ||||||
|                                         <form action="{% url 'deployment-launch' deployment.id %}?page={{ page_obj.number }}" method="post"> |                                         <form action="{% url 'deployment-launch' deployment.id %}?page={{ page_obj.number }}" method="post"> | ||||||
|                                             {% csrf_token %} |                                             {% csrf_token %} | ||||||
|                                             <button class="btn btn-success" type="submit">Deploy</button> |                                             <button class="btn btn-success btn-sm" type="submit">Deploy</button> | ||||||
|                                         </form> |                                         </form> | ||||||
|                                     </th> |                                     </th> | ||||||
|                                 {% elif deployment.status == "RUNNING" %} |                                 {% elif deployment.status == "RUNNING" %} | ||||||
|                                     <th name="deploy"> |                                     <th name="deploy"> | ||||||
|                                         <button class="btn btn-primary" type="button" disabled> |                                         <button class="btn btn-primary btn-sm" type="button" disabled> | ||||||
|                                             <span class="spinner-border spinner-border-sm" aria-hidden="true"></span> |                                             <span class="spinner-border spinner-border-sm" aria-hidden="true"></span> | ||||||
|                                             <span role="status">Deploy</span> |                                             <span role="status">Deploying...</span> | ||||||
|  |                                         </button> | ||||||
|  |                                     </th> | ||||||
|  |                                 {% elif deployment.status == "PENDING" %} | ||||||
|  |                                     <th name="deploy"> | ||||||
|  |                                         <button class="btn btn-primary btn-sm" type="button" disabled> | ||||||
|  |                                             <span role="status">Pending...</span> | ||||||
|                                         </button> |                                         </button> | ||||||
|                                     </th> |                                     </th> | ||||||
|                                 {% else %} |                                 {% else %} | ||||||
| @ -58,9 +64,12 @@ | |||||||
|                         {% endfor %} |                         {% endfor %} | ||||||
|                     </table> |                     </table> | ||||||
|                     {% include 'pagination.html' %} |                     {% include 'pagination.html' %} | ||||||
|                     <button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#create-deployment-modal"> |                     <button type="button" {% if can_create %} class="btn btn-success" data-bs-toggle="modal" data-bs-target="#create-deployment-modal" {% else %} class="btn btn-danger" disabled {% endif %}> | ||||||
|                         Create |                         Create | ||||||
|                     </button>                   |                     </button> | ||||||
|  |                     {% if not can_create %} | ||||||
|  |                         <p style="font-style: italic;">You can't create new deployments, contact administrator for support.</p> | ||||||
|  |                     {% endif %} | ||||||
|                 {% else %} |                 {% else %} | ||||||
|                     <h4 style="text-align: center;">Please log in !</h4> |                     <h4 style="text-align: center;">Please log in !</h4> | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
| @ -70,7 +79,9 @@ | |||||||
| {% endblock %} | {% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block modal %} | {% block modal %} | ||||||
|     {% include 'deployment/create_modal.html' %} |     {% if can_create %} | ||||||
|  |         {% include 'deployment/create_modal.html' %} | ||||||
|  |     {% endif %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block script %} | {% block script %} | ||||||
|  | |||||||
| @ -1,7 +1,23 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
| 
 | 
 | ||||||
|  | {% load static %} | ||||||
|  | 
 | ||||||
| {% block title %} Deployment details: {{ deployment.name }} {% endblock %} | {% block title %} Deployment details: {{ deployment.name }} {% endblock %} | ||||||
| 
 | 
 | ||||||
|  | {% block bodyattr %} | ||||||
|  |         {% if deployment.status == "RUNNING" or deployment.status == "PENDING" %} | ||||||
|  |             onload="start('{{ url|safe }}');" | ||||||
|  |         {% endif %} | ||||||
|  | {% endblock %} | ||||||
|  | 
 | ||||||
|  | {% block headscript %} | ||||||
|  |     {% if deployment.status == "RUNNING" or deployment.status == "PENDING" %}    | ||||||
|  |         <script src="{% static 'django_eventstream/json2.js' %}"></script> | ||||||
|  |         <script src="{% static 'django_eventstream/eventsource.min.js' %}"></script> | ||||||
|  |         <script src="{% static 'django_eventstream/reconnecting-eventsource.js' %}"></script> | ||||||
|  |     {% endif %} | ||||||
|  | {% endblock %} | ||||||
|  | 
 | ||||||
| {% block content %} | {% block content %} | ||||||
|     <div class="container-fluid"> |     <div class="container-fluid"> | ||||||
|         <div class="row justify-content-md-center"> |         <div class="row justify-content-md-center"> | ||||||
| @ -19,21 +35,48 @@ | |||||||
|                         <input type="text" readonly class="form-control-plaintext" id="status" value="{{ deployment.status }}"> |                         <input type="text" readonly class="form-control-plaintext" id="status" value="{{ deployment.status }}"> | ||||||
|                         <label for="status">Status</label> |                         <label for="status">Status</label> | ||||||
|                     </div> |                     </div> | ||||||
|  |                     {% if deployment.status == "RUNNING" or deployment.status == "PENDING" %} | ||||||
|  |                         <div class="form-floating mb-3"> | ||||||
|  |                             <div class="progress" role="progressbar" aria-label="deployment-progress" aria-valuenow="{{ deployment.progress }}" aria-valuemin="0" aria-valuemax="100"> | ||||||
|  |                                 <div id="deployment-progress" class="progress-bar progress-bar-striped progress-bar-animated" style="width: {{ deployment.progress }}%"></div> | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     {% endif %} | ||||||
|                     <div class="form-floating mb-3"> |                     <div class="form-floating mb-3"> | ||||||
|                         <input type="text" readonly class="form-control-plaintext" id="created_at" value="{{ deployment.created_at }}"> |                         <input type="text" readonly class="form-control-plaintext" id="created_at" value="{{ deployment.created_at }}"> | ||||||
|                         <label for="created-at">Created at</label> |                         <label for="created-at">Created at</label> | ||||||
|                     </div> |                     </div> | ||||||
|                     <div class="form-floating mb-3"> |                     <div class="form-floating mb-3"> | ||||||
|                         <input type="text" readonly class="form-control-plaintext" id="updated_at" value="{{ deployment.updated_at }}"> |                         <input type="text" readonly class="form-control-plaintext" id="updated_at" value="{{ deployment.updated_at }}"> | ||||||
|                         <label for="updated-at">{{ deployment.updated_at }}</label> |                         <label for="updated-at">Updated at</label> | ||||||
|                     </div> |                     </div> | ||||||
|                     <form action="" method="post"> |                     {% if deployment.status == "RUNNING" %} | ||||||
|                         {% csrf_token %} |                         <div class="form-floating mb-3"> | ||||||
|                         <button type="button" onclick="goBack()" class="btn btn-secondary">Back</button> |                             <input type="text" readonly class="form-control-plaintext" id="updated_at" value="{{ deployment.task_id }}"> | ||||||
|                         {% if deployment.status != "RUNNING" %} |                             <label for="updated-at">Task UUID</label> | ||||||
|                             <button type="submit" class="btn btn-danger">Delete</button> |                         </div> | ||||||
|                         {% endif %} |                     {% endif %} | ||||||
|                     </form> |                     {% if deployment.status == "FAILED" %} | ||||||
|  |                         <div class="form-floating mb-3"> | ||||||
|  |                             <input type="text" readonly class="form-control-plaintext" id="updated_at" value="{{ deployment.error }}"> | ||||||
|  |                             <label for="updated-at">Error</label> | ||||||
|  |                         </div> | ||||||
|  |                     {% endif %} | ||||||
|  |                     {% if deployment.status != "RUNNING" and deployment.status != "PENDING" %} | ||||||
|  |                         <form id="delete-deployment" action="" method="post"> | ||||||
|  |                             {% csrf_token %} | ||||||
|  |                         </form> | ||||||
|  |                     {% else %} | ||||||
|  |                         <form id="abort-deployment" action="{% url 'deployment-abort' deployment.id %}" method="post"> | ||||||
|  |                             {% csrf_token %} | ||||||
|  |                         </form> | ||||||
|  |                     {% endif %} | ||||||
|  |                     <button type="button" onclick="goBack()" class="btn btn-secondary">Back</button> | ||||||
|  |                     {% if deployment.status != "RUNNING" and deployment.status != "PENDING" %} | ||||||
|  |                         <button form="delete-deployment" type="submit" class="btn btn-danger">Delete</button> | ||||||
|  |                     {% else %} | ||||||
|  |                         <button form="abort-deployment" type="submit" class="btn btn-danger">Abort</button> | ||||||
|  |                     {% endif %} | ||||||
|                 {% else %} |                 {% else %} | ||||||
|                     <h4 style="text-align: center;">Please log in !</h4> |                     <h4 style="text-align: center;">Please log in !</h4> | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
| @ -43,9 +86,13 @@ | |||||||
| {% endblock %} | {% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block script %} | {% block script %} | ||||||
| <script> |     {% if deployment.status == "RUNNING" or deployment.status == "PENDING" %} | ||||||
|     function goBack() { |         <script src="{% static 'deployment/js/event_source_details.js' %}" /> | ||||||
|         window.history.back(); |         </script> | ||||||
|     } |     {% endif %} | ||||||
| </script> |     <script> | ||||||
|  |         function goBack() { | ||||||
|  |             window.location={% url 'deployments' %}; | ||||||
|  |         } | ||||||
|  |     </script> | ||||||
| {% endblock %} | {% endblock %} | ||||||
| @ -1,6 +1,9 @@ | |||||||
|  | {% load static %} | ||||||
|  | 
 | ||||||
| <header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between mb-4 border-bottom"> | <header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between mb-4 border-bottom"> | ||||||
|     <nav class="navbar navbar-expand-lg bg-dark border-bottom border-body" data-bs-theme="dark" style="width: 100%;"> |     <nav class="navbar navbar-expand-lg bg-dark border-bottom border-body" data-bs-theme="dark" style="width: 100%;"> | ||||||
|         <div class="container-fluid"> |         <div class="container-fluid"> | ||||||
|  |             <img src="{% static 'deployment/png/flash-icon.png' %}" alt="Logo" width="40" height="40" class="d-inline-block align-text-top"> | ||||||
|             <a class="navbar-brand" href="/">Mumui</a> |             <a class="navbar-brand" href="/">Mumui</a> | ||||||
|             <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" |             <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" | ||||||
|                 aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"> |                 aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"> | ||||||
| @ -12,7 +15,7 @@ | |||||||
|                         <a class="nav-link" href="/">Home</a> |                         <a class="nav-link" href="/">Home</a> | ||||||
|                     </li> |                     </li> | ||||||
|                     <li class="nav-item"> |                     <li class="nav-item"> | ||||||
|                         <a href="{% url 'deployment' %}" class="nav-link">Deployments</a></li> |                         <a href="{% url 'deployments' %}" class="nav-link">Deployments</a></li> | ||||||
|                     </li> |                     </li> | ||||||
|                     <li class="nav-item"> |                     <li class="nav-item"> | ||||||
|                         <a class="nav-link" href="#">About</a></li> |                         <a class="nav-link" href="#">About</a></li> | ||||||
| @ -26,35 +29,4 @@ | |||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|     </nav> |     </nav> | ||||||
| </header> | </header> | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| <!-- <nav class="navbar navbar-expand-lg bg-dark border-bottom border-body" data-bs-theme="dark"> |  | ||||||
|     <div class="container-fluid"> |  | ||||||
|         <a class="navbar-brand" href="#">Navbar</a> |  | ||||||
|         <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarColor01" |  | ||||||
|             aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation"> |  | ||||||
|             <span class="navbar-toggler-icon"></span> |  | ||||||
|         </button> |  | ||||||
|         <div class="collapse navbar-collapse" id="navbarColor01"> |  | ||||||
|             <ul class="navbar-nav me-auto mb-2 mb-lg-0"> |  | ||||||
|                 <li class="nav-item"> |  | ||||||
|                     <a class="nav-link active" aria-current="page" href="#">Home</a> |  | ||||||
|                 </li> |  | ||||||
|                 <li class="nav-item"> |  | ||||||
|                     <a class="nav-link" href="#">Features</a> |  | ||||||
|                 </li> |  | ||||||
|                 <li class="nav-item"> |  | ||||||
|                     <a class="nav-link" href="#">Pricing</a> |  | ||||||
|                 </li> |  | ||||||
|                 <li class="nav-item"> |  | ||||||
|                     <a class="nav-link" href="#">About</a> |  | ||||||
|                 </li> |  | ||||||
|             </ul> |  | ||||||
|             <form class="d-flex" role="search"> |  | ||||||
|                 <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search"> |  | ||||||
|                 <button class="btn btn-outline-light" type="submit">Search</button> |  | ||||||
|             </form> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </nav> --> |  | ||||||
| @ -13,7 +13,7 @@ | |||||||
|             <div class="row justify-content-md-center"> |             <div class="row justify-content-md-center"> | ||||||
|                 <div class="col-lg-6 col-md-8 col-sm-8"> |                 <div class="col-lg-6 col-md-8 col-sm-8"> | ||||||
|                     <div style="text-align: center;">Forget, i don't care... Please go on  |                     <div style="text-align: center;">Forget, i don't care... Please go on  | ||||||
|                         <a href="/deployment/">Deployments</a>  |                         <a href="{% url 'deployments' %}">Deployments</a>  | ||||||
|                         and enjoy the life ! |                         and enjoy the life ! | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user