Compare commits
	
		
			No commits in common. "main" and "feat/ui-upgrade" have entirely different histories.
		
	
	
		
			main
			...
			feat/ui-up
		
	
		
							
								
								
									
										12
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								.env.example
									
									
									
									
									
								
							| @ -1,12 +0,0 @@ | |||||||
| # '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 |  | ||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -5,5 +5,3 @@ venv | |||||||
| /static | /static | ||||||
| *.log | *.log | ||||||
| *.pid | *.pid | ||||||
| docker-compose.override.yml |  | ||||||
| .env |  | ||||||
							
								
								
									
										12
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,8 +1,12 @@ | |||||||
|  | # TODO(rmanach): add a pyproject.toml
 | ||||||
|  | 
 | ||||||
| format: | format: | ||||||
| 	@./venv/bin/black mumui/*.py deployment/*.py | 	black deployment/*.py | ||||||
|  | 	black mumui/*.py | ||||||
| 
 | 
 | ||||||
| lint: | lint: | ||||||
| 	@./venv/bin/ruff . | 	ruff deployment/*.py | ||||||
|  | 	ruff mumui/*.py | ||||||
| 
 | 
 | ||||||
| dev: | dev: | ||||||
| 	rm -rf venv | 	rm -rf venv | ||||||
| @ -14,10 +18,10 @@ django: | |||||||
| 	docker build . -t mumui:local | 	docker build . -t mumui:local | ||||||
| 
 | 
 | ||||||
| pushpin-local: | pushpin-local: | ||||||
| 	cd pushpin && docker build . -t pushpin:mumui | 	cd pushpin && docker build . -t pushpin:local | ||||||
| 
 | 
 | ||||||
| nginx-local: | nginx-local: | ||||||
| 	cd nginx && docker build . -t nginx:mumui | 	cd nginx && docker build . -t nginx:local | ||||||
| 
 | 
 | ||||||
| build: | build: | ||||||
| 	$(MAKE) pushpin-local | 	$(MAKE) pushpin-local | ||||||
|  | |||||||
| @ -6,12 +6,6 @@ | |||||||
| 
 | 
 | ||||||
| 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: | ||||||
| 
 | 
 | ||||||
| @ -30,4 +24,6 @@ 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,15 +1,3 @@ | |||||||
| from django.contrib import admin | # from django.contrib import admin | ||||||
| 
 | 
 | ||||||
| from deployment.models import Deployment, DeploymentUser | # Register your models here. | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class DeploymentUserAdmin(admin.ModelAdmin): |  | ||||||
|     pass |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class DeploymentAdmin(admin.ModelAdmin): |  | ||||||
|     pass |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| admin.site.register(Deployment, DeploymentAdmin) |  | ||||||
| admin.site.register(DeploymentUser, DeploymentUserAdmin) |  | ||||||
|  | |||||||
| @ -4,8 +4,3 @@ 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() |  | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								deployment/channel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								deployment/channel.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | 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 | ||||||
| @ -1,51 +0,0 @@ | |||||||
| 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-22 14:39 | # Generated by Django 4.2.5 on 2023-09-20 17:23 | ||||||
| 
 | 
 | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
| @ -46,8 +46,6 @@ 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( | ||||||
|  | |||||||
| @ -1,25 +0,0 @@ | |||||||
| # 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,9 +1,8 @@ | |||||||
| from enum import Enum | from enum import Enum | ||||||
| from typing import Any | from datetime import datetime | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -29,21 +28,6 @@ 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) | ||||||
| @ -55,13 +39,7 @@ 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) |  | ||||||
|  | |||||||
| @ -1,27 +0,0 @@ | |||||||
| 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,6 @@ | |||||||
| var start = function (url) { | var start = function (url) { | ||||||
|     var es = new ReconnectingEventSource(url); |  | ||||||
| 
 | 
 | ||||||
|     console.log("url: " + url); |     var es = new ReconnectingEventSource(url); | ||||||
| 
 | 
 | ||||||
|     es.onopen = function () { |     es.onopen = function () { | ||||||
|         console.log('connected'); |         console.log('connected'); | ||||||
| @ -30,34 +29,14 @@ 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); | ||||||
| }; | }; | ||||||
| @ -1,43 +0,0 @@ | |||||||
| 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); |  | ||||||
| }; |  | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 27 KiB | 
| @ -3,100 +3,30 @@ import random | |||||||
| from uuid import UUID | from uuid import UUID | ||||||
| 
 | 
 | ||||||
| from celery import shared_task | from celery import shared_task | ||||||
| from celery.contrib.abortable import AbortableTask | from django_eventstream import send_event | ||||||
| 
 | 
 | ||||||
| from deployment.channels import Event |  | ||||||
| from deployment.models import Deployment, Type, Status | from deployment.models import Deployment, Type, Status | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DeploymentTask(AbortableTask): | @shared_task | ||||||
|     def try_exec(self, stop: int): | def deploy(deployment_id: UUID): | ||||||
|         if stop == 0: |     deploy = Deployment.objects.get(id=deployment_id) | ||||||
|             stop = 1 |  | ||||||
| 
 | 
 | ||||||
|         for i in range(1, stop): |     deploy.status = Status.RUNNING.name | ||||||
|             time.sleep(5 * stop - 2 * i) |     deploy.save() | ||||||
| 
 | 
 | ||||||
|             if random.randint(0, 10) == 10: |     match deploy.type: | ||||||
|                 raise Exception("unable to perform the deployment") |         case Type.SLIM.name: | ||||||
|  |             time.sleep(10) | ||||||
|  |         case Type.MEDIUM.name: | ||||||
|  |             time.sleep(60) | ||||||
|  |         case Type.LARGE.name: | ||||||
|  |             time.sleep(120) | ||||||
| 
 | 
 | ||||||
|             yield i |     deploy.status = ( | ||||||
|  |         Status.FAILED.name if random.randint(0, 10) % 2 != 0 else Status.SUCCESS.name | ||||||
|  |     ) | ||||||
|  |     deploy.save() | ||||||
| 
 | 
 | ||||||
|     def run_slim(self): |     send_event("deployment", "message", {"id": deploy.id, "status": deploy.status}) | ||||||
|         progress = 95 |     print("event sent") | ||||||
|         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,23 +1,17 @@ | |||||||
| 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, abort | from deployment.views import index, create, details, deploy | ||||||
| 
 | 
 | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path("", index, name="deployments"), |     path("", index, name="deployment"), | ||||||
|     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/<user_id>/", |         "events/", | ||||||
|         include(django_eventstream.urls), |         include(django_eventstream.urls), | ||||||
|         {"format-channels": ["deployment_{user_id}"]}, |         {"channels": ["deployment"]}, | ||||||
|         name="deployments-events", |         name="deployment-events", | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "events/<user_id>/<deployment_id>/", |  | ||||||
|         include(django_eventstream.urls), |  | ||||||
|         {"format-channels": ["deployment_{user_id}_{deployment_id}"]}, |  | ||||||
|     ), |     ), | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -1,39 +1,18 @@ | |||||||
| 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" | ||||||
| @ -48,9 +27,8 @@ 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": f"events/{request.user.id}/", |             "url": "events/", | ||||||
|             "can_create": check_user_credits(request.user), |  | ||||||
|         }, |         }, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| @ -59,90 +37,40 @@ 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"/deployments?page={page}") |         return HttpResponseRedirect(f"/deployment?page={page}") | ||||||
| 
 | 
 | ||||||
|     return HttpResponseRedirect("/deployments") |     return HttpResponseRedirect("/deployment") | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 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("/deployments") |         return HttpResponseRedirect("/deployment") | ||||||
| 
 | 
 | ||||||
|     return render( |     return render(request, "deployment/details.html", {"deployment": deployment}) | ||||||
|         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 not form.is_valid(): |         if form.is_valid(): | ||||||
|             return HttpResponseBadRequest( |             try: | ||||||
|                 f"deployment creation inputs are invalid: {form.errors}" |                 Deployment.objects.create( | ||||||
|             ) |                     user=request.user, id=uuid4(), **form.cleaned_data | ||||||
|  |                 ) | ||||||
|  |             except Exception as e: | ||||||
|  |                 return HttpResponseServerError(e) | ||||||
|              |              | ||||||
|         try: |     return HttpResponseRedirect("/deployment") | ||||||
|             Deployment.objects.create( |  | ||||||
|                 user=request.user, id=uuid4(), **form.cleaned_data |  | ||||||
|             ) |  | ||||||
|         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-mumui |     container_name: redis | ||||||
|     networks: |     networks: | ||||||
|       - mumui_network |       - mumui_network | ||||||
|     volumes: |     volumes: | ||||||
|       - redis_data:/data |       - redis_data:/data | ||||||
| 
 | 
 | ||||||
|   pushpin: |   pushpin: | ||||||
|     image: pushpin:mumui |     image: pushpin:local | ||||||
|     container_name: pushpin-mumui |     container_name: pushpin | ||||||
|     networks: |     networks: | ||||||
|       - mumui_network |       - mumui_network | ||||||
|     depends_on: |     depends_on: | ||||||
| @ -18,9 +18,11 @@ services: | |||||||
| 
 | 
 | ||||||
|   postgres: |   postgres: | ||||||
|     image: postgres:latest |     image: postgres:latest | ||||||
|     container_name: postgres-mumui |     container_name: postgres | ||||||
|     env_file: |     environment: | ||||||
|       - .env |       POSTGRES_DB: mumui | ||||||
|  |       POSTGRES_USER: test | ||||||
|  |       POSTGRES_PASSWORD: test | ||||||
|     networks: |     networks: | ||||||
|       - mumui_network |       - mumui_network | ||||||
|     volumes: |     volumes: | ||||||
| @ -29,8 +31,6 @@ 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:mumui |     image: nginx:local | ||||||
|     container_name: nginx-mumui |     container_name: nginx | ||||||
|     networks: |     networks: | ||||||
|       - mumui_network |       - mumui_network | ||||||
|     volumes: |     volumes: | ||||||
| @ -50,6 +50,7 @@ services: | |||||||
|     depends_on: |     depends_on: | ||||||
|       - mumui |       - mumui | ||||||
|    |    | ||||||
|  | 
 | ||||||
| networks: | networks: | ||||||
|   mumui_network: |   mumui_network: | ||||||
|     driver: bridge |     driver: bridge | ||||||
|  | |||||||
| @ -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", backend="redis://redis:6379/0") | app = Celery("mumui", broker="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,7 +9,6 @@ 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 | ||||||
| 
 | 
 | ||||||
| @ -23,13 +22,10 @@ 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" | ||||||
| 
 | 
 | ||||||
| IS_PROD = os.getenv("ENV") == "prod" |  | ||||||
| HOST = "*" if not IS_PROD else os.getenv("HOST", "*") |  | ||||||
| 
 |  | ||||||
| # SECURITY WARNING: don't run with debug turned on in production! | # SECURITY WARNING: don't run with debug turned on in production! | ||||||
| DEBUG = not IS_PROD | DEBUG = True | ||||||
| 
 | 
 | ||||||
| ALLOWED_HOSTS = ["*"] if not IS_PROD else [HOST.lstrip("https://")] | ALLOWED_HOSTS = ["*"] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Application definition | # Application definition | ||||||
| @ -84,8 +80,8 @@ DATABASES = { | |||||||
|     "default": { |     "default": { | ||||||
|         "ENGINE": "django.db.backends.postgresql", |         "ENGINE": "django.db.backends.postgresql", | ||||||
|         "NAME": "mumui", |         "NAME": "mumui", | ||||||
|         "USER": os.getenv("POSTGRES_USER", "test"), |         "USER": "test", | ||||||
|         "PASSWORD": os.getenv("POSTGRES_PASSWORD", "test"), |         "PASSWORD": "test", | ||||||
|         "HOST": "postgres", |         "HOST": "postgres", | ||||||
|         "PORT": "5432", |         "PORT": "5432", | ||||||
|     } |     } | ||||||
| @ -137,6 +133,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.channels.DeploymentChannelManager" | EVENTSTREAM_CHANNELMANAGER_CLASS = "deployment.channel.DeploymentChannelManager" | ||||||
| 
 | 
 | ||||||
| CSRF_TRUSTED_ORIGINS = [HOST] if IS_PROD else ["http://localhost:8080"] | CSRF_TRUSTED_ORIGINS = ["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("deployments/", include("deployment.urls")), |     path("deployment/", include("deployment.urls")), | ||||||
|     path("", TemplateView.as_view(template_name="home.html"), name="home"), |     path("", TemplateView.as_view(template_name="home.html"), name="home"), | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -1,19 +0,0 @@ | |||||||
| [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_PASSWORD} python manage.py createsuperuser --noinput --username admin --email admin@admin.fr | DJANGO_SUPERUSER_PASSWORD=admin 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-10 col-sm-12"> |             <div class="col-lg-8 col-md-8 col-sm-6"> | ||||||
|                 {% 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-1"></th> |                             <th class="col-2"></th> | ||||||
|                             <th class="col-2"></th> |                             <th class="col-2"></th> | ||||||
|                         </tr> |                         </tr> | ||||||
|                         {% for deployment in page_obj %} |                         {% for deployment in page_obj %} | ||||||
| @ -34,27 +34,21 @@ | |||||||
|                                 <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 btn-sm">Details</button> |                                         <button class="btn btn-primary">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 btn-sm" type="submit">Deploy</button> |                                             <button class="btn btn-success" 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 btn-sm" type="button" disabled> |                                         <button class="btn btn-primary" 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">Deploying...</span> |                                             <span role="status">Deploy</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 %} | ||||||
| @ -64,12 +58,9 @@ | |||||||
|                         {% endfor %} |                         {% endfor %} | ||||||
|                     </table> |                     </table> | ||||||
|                     {% include 'pagination.html' %} |                     {% include 'pagination.html' %} | ||||||
|                     <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 %}> |                     <button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#create-deployment-modal"> | ||||||
|                         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 %} | ||||||
| @ -79,9 +70,7 @@ | |||||||
| {% endblock %} | {% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block modal %} | {% block modal %} | ||||||
|     {% if can_create %} |     {% include 'deployment/create_modal.html' %} | ||||||
|         {% include 'deployment/create_modal.html' %} |  | ||||||
|     {% endif %} |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block script %} | {% block script %} | ||||||
|  | |||||||
| @ -1,23 +1,7 @@ | |||||||
| {% 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"> | ||||||
| @ -35,48 +19,21 @@ | |||||||
|                         <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">Updated at</label> |                         <label for="updated-at">{{ deployment.updated_at }}</label> | ||||||
|                     </div> |                     </div> | ||||||
|                     {% if deployment.status == "RUNNING" %} |                     <form action="" method="post"> | ||||||
|                         <div class="form-floating mb-3"> |                         {% csrf_token %} | ||||||
|                             <input type="text" readonly class="form-control-plaintext" id="updated_at" value="{{ deployment.task_id }}"> |                         <button type="button" onclick="goBack()" class="btn btn-secondary">Back</button> | ||||||
|                             <label for="updated-at">Task UUID</label> |                         {% if deployment.status != "RUNNING" %} | ||||||
|                         </div> |                             <button type="submit" class="btn btn-danger">Delete</button> | ||||||
|                     {% endif %} |                         {% endif %} | ||||||
|                     {% if deployment.status == "FAILED" %} |                     </form> | ||||||
|                         <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 %} | ||||||
| @ -86,13 +43,9 @@ | |||||||
| {% endblock %} | {% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block script %} | {% block script %} | ||||||
|     {% if deployment.status == "RUNNING" or deployment.status == "PENDING" %} | <script> | ||||||
|         <script src="{% static 'deployment/js/event_source_details.js' %}" /> |     function goBack() { | ||||||
|         </script> |         window.history.back(); | ||||||
|     {% endif %} |     } | ||||||
|     <script> | </script> | ||||||
|         function goBack() { |  | ||||||
|             window.location={% url 'deployments' %}; |  | ||||||
|         } |  | ||||||
|     </script> |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
| @ -1,9 +1,6 @@ | |||||||
| {% 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"> | ||||||
| @ -15,7 +12,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 'deployments' %}" class="nav-link">Deployments</a></li> |                         <a href="{% url 'deployment' %}" 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> | ||||||
| @ -30,3 +27,34 @@ | |||||||
|         </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="{% url 'deployments' %}">Deployments</a>  |                         <a href="/deployment/">Deployments</a>  | ||||||
|                         and enjoy the life ! |                         and enjoy the life ! | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user