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 | ||||
| *.log | ||||
| *.pid | ||||
| docker-compose.override.yml | ||||
| .env | ||||
							
								
								
									
										12
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,8 +1,12 @@ | ||||
| # TODO(rmanach): add a pyproject.toml
 | ||||
| 
 | ||||
| format: | ||||
| 	@./venv/bin/black mumui/*.py deployment/*.py | ||||
| 	black deployment/*.py | ||||
| 	black mumui/*.py | ||||
| 
 | ||||
| lint: | ||||
| 	@./venv/bin/ruff . | ||||
| 	ruff deployment/*.py | ||||
| 	ruff mumui/*.py | ||||
| 
 | ||||
| dev: | ||||
| 	rm -rf venv | ||||
| @ -14,10 +18,10 @@ django: | ||||
| 	docker build . -t mumui:local | ||||
| 
 | ||||
| pushpin-local: | ||||
| 	cd pushpin && docker build . -t pushpin:mumui | ||||
| 	cd pushpin && docker build . -t pushpin:local | ||||
| 
 | ||||
| nginx-local: | ||||
| 	cd nginx && docker build . -t nginx:mumui | ||||
| 	cd nginx && docker build . -t nginx:local | ||||
| 
 | ||||
| build: | ||||
| 	$(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). | ||||
| 
 | ||||
| You can test the application here: [mumui.thegux.fr](https://mumui.thegux.fr). | ||||
| * username: **demo** | ||||
| * password: **demo1234** | ||||
| 
 | ||||
| Enjoy ! 😛 | ||||
| 
 | ||||
| ## 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. | ||||
| 
 | ||||
| Enjoy ! 😛 | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
| 
 | ||||
| class DeploymentUserAdmin(admin.ModelAdmin): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class DeploymentAdmin(admin.ModelAdmin): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| admin.site.register(Deployment, DeploymentAdmin) | ||||
| admin.site.register(DeploymentUser, DeploymentUserAdmin) | ||||
| # Register your models here. | ||||
|  | ||||
| @ -4,8 +4,3 @@ from django.apps import AppConfig | ||||
| class DeploymentConfig(AppConfig): | ||||
|     default_auto_field = "django.db.models.BigAutoField" | ||||
|     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.db import migrations, models | ||||
| @ -46,8 +46,6 @@ class Migration(migrations.Migration): | ||||
|                 ), | ||||
|                 ("created_at", models.DateTimeField(auto_now_add=True)), | ||||
|                 ("updated_at", models.DateTimeField(auto_now=True)), | ||||
|                 ("error", models.TextField(null=True)), | ||||
|                 ("task_id", models.UUIDField(null=True)), | ||||
|                 ( | ||||
|                     "user", | ||||
|                     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 typing import Any | ||||
| from datetime import datetime | ||||
| 
 | ||||
| 
 | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.validators import MinValueValidator | ||||
| from django.db import models | ||||
| 
 | ||||
| 
 | ||||
| @ -29,21 +28,6 @@ class Status(Enum): | ||||
|         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): | ||||
|     id = models.UUIDField(primary_key=True) | ||||
|     name = models.TextField(null=False, blank=False) | ||||
| @ -55,13 +39,7 @@ class Deployment(models.Model): | ||||
|     ) | ||||
|     created_at = models.DateTimeField(auto_now_add=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) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         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 es = new ReconnectingEventSource(url); | ||||
| 
 | ||||
|     console.log("url: " + url); | ||||
|     var es = new ReconnectingEventSource(url); | ||||
| 
 | ||||
|     es.onopen = function () { | ||||
|         console.log('connected'); | ||||
| @ -30,27 +29,6 @@ var start = function (url) { | ||||
|             status.innerHTML = message.status; | ||||
| 
 | ||||
|             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") { | ||||
|                 button.innerHTML = ""; | ||||
|             } | ||||
| @ -59,5 +37,6 @@ var start = function (url) { | ||||
|                     window.location.reload(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     }, 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 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 | ||||
| 
 | ||||
| 
 | ||||
| class DeploymentTask(AbortableTask): | ||||
|     def try_exec(self, stop: int): | ||||
|         if stop == 0: | ||||
|             stop = 1 | ||||
| @shared_task | ||||
| def deploy(deployment_id: UUID): | ||||
|     deploy = Deployment.objects.get(id=deployment_id) | ||||
| 
 | ||||
|         for i in range(1, stop): | ||||
|             time.sleep(5 * stop - 2 * i) | ||||
|     deploy.status = Status.RUNNING.name | ||||
|     deploy.save() | ||||
| 
 | ||||
|             if random.randint(0, 10) == 10: | ||||
|                 raise Exception("unable to perform the deployment") | ||||
| 
 | ||||
|             yield i | ||||
| 
 | ||||
|     def run_slim(self): | ||||
|         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: | ||||
|     match deploy.type: | ||||
|         case Type.SLIM.name: | ||||
|                 self.run_slim() | ||||
|             time.sleep(10) | ||||
|         case Type.MEDIUM.name: | ||||
|                 self.run_medium() | ||||
|             time.sleep(60) | ||||
|         case Type.LARGE.name: | ||||
|                 self.run_large() | ||||
|             time.sleep(120) | ||||
| 
 | ||||
|         self.deploy.task_id = None | ||||
|         self.deploy.save() | ||||
|     deploy.status = ( | ||||
|         Status.FAILED.name if random.randint(0, 10) % 2 != 0 else Status.SUCCESS.name | ||||
|     ) | ||||
|     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) | ||||
|     send_event("deployment", "message", {"id": deploy.id, "status": deploy.status}) | ||||
|     print("event sent") | ||||
|  | ||||
| @ -1,23 +1,17 @@ | ||||
| import django_eventstream | ||||
| 
 | ||||
| from django.urls import path, include | ||||
| from deployment.views import index, create, details, deploy, abort | ||||
| from deployment.views import index, create, details, deploy | ||||
| 
 | ||||
| urlpatterns = [ | ||||
|     path("", index, name="deployments"), | ||||
|     path("", index, name="deployment"), | ||||
|     path("create", create, name="deployment-create"), | ||||
|     path("<uuid:deployment_id>", details, name="deployment-details"), | ||||
|     path("<uuid:deployment_id>/deploy", deploy, name="deployment-launch"), | ||||
|     path("<uuid:deployment_id>/abort", abort, name="deployment-abort"), | ||||
|     path( | ||||
|         "events/<user_id>/", | ||||
|         "events/", | ||||
|         include(django_eventstream.urls), | ||||
|         {"format-channels": ["deployment_{user_id}"]}, | ||||
|         name="deployments-events", | ||||
|     ), | ||||
|     path( | ||||
|         "events/<user_id>/<deployment_id>/", | ||||
|         include(django_eventstream.urls), | ||||
|         {"format-channels": ["deployment_{user_id}_{deployment_id}"]}, | ||||
|         {"channels": ["deployment"]}, | ||||
|         name="deployment-events", | ||||
|     ), | ||||
| ] | ||||
|  | ||||
| @ -1,39 +1,18 @@ | ||||
| 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.http import ( | ||||
|     HttpResponseRedirect, | ||||
|     HttpResponseServerError, | ||||
|     HttpResponseBadRequest, | ||||
|     HttpResponseForbidden, | ||||
| ) | ||||
| from django.shortcuts import render, get_object_or_404 | ||||
| 
 | ||||
| from deployment.channels import Event | ||||
| from deployment.forms import DeploymentForm | ||||
| from deployment.models import Deployment, Status | ||||
| 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): | ||||
|     deployments = Deployment.objects.filter(user=request.user.id).order_by( | ||||
|         "-created_at" | ||||
| @ -48,9 +27,8 @@ def index(request): | ||||
|         "deployment/board.html", | ||||
|         { | ||||
|             "page_obj": page_obj, | ||||
|             "range_pages": (i + 1 for i in range(page_obj.paginator.num_pages)), | ||||
|             "url": f"events/{request.user.id}/", | ||||
|             "can_create": check_user_credits(request.user), | ||||
|             "range_pages": [i + 1 for i in range(page_obj.paginator.num_pages)], | ||||
|             "url": "events/", | ||||
|         }, | ||||
|     ) | ||||
| 
 | ||||
| @ -59,85 +37,35 @@ def deploy(request, deployment_id): | ||||
|     deployment = get_object_or_404(Deployment, id=deployment_id) | ||||
| 
 | ||||
|     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.save() | ||||
|         launch_deploy.delay(deployment_id) | ||||
| 
 | ||||
|     if page := request.GET.get("page", ""): | ||||
|         return HttpResponseRedirect(f"/deployments?page={page}") | ||||
|         return HttpResponseRedirect(f"/deployment?page={page}") | ||||
| 
 | ||||
|     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}") | ||||
|     return HttpResponseRedirect("/deployment") | ||||
| 
 | ||||
| 
 | ||||
| def details(request, 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 deployment.status == Status.RUNNING.name: | ||||
|             return HttpResponseBadRequest("deployment is running") | ||||
| 
 | ||||
|         if deployment.status == Status.PENDING.name: | ||||
|             return HttpResponseBadRequest("deployment is pending") | ||||
| 
 | ||||
|         try: | ||||
|             deployment.delete() | ||||
|         except Exception as e: | ||||
|             return HttpResponseServerError(e) | ||||
|         return HttpResponseRedirect("/deployments") | ||||
|         return HttpResponseRedirect("/deployment") | ||||
| 
 | ||||
|     return render( | ||||
|         request, | ||||
|         "deployment/details.html", | ||||
|         {"deployment": deployment, "url": f"events/{request.user.id}/{deployment.id}/"}, | ||||
|     ) | ||||
|     return render(request, "deployment/details.html", {"deployment": deployment}) | ||||
| 
 | ||||
| 
 | ||||
| def create(request): | ||||
|     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) | ||||
|         if not form.is_valid(): | ||||
|             return HttpResponseBadRequest( | ||||
|                 f"deployment creation inputs are invalid: {form.errors}" | ||||
|             ) | ||||
| 
 | ||||
|         if form.is_valid(): | ||||
|             try: | ||||
|                 Deployment.objects.create( | ||||
|                     user=request.user, id=uuid4(), **form.cleaned_data | ||||
| @ -145,4 +73,4 @@ def create(request): | ||||
|             except Exception as e: | ||||
|                 return HttpResponseServerError(e) | ||||
|              | ||||
|     return HttpResponseRedirect("/deployments") | ||||
|     return HttpResponseRedirect("/deployment") | ||||
|  | ||||
| @ -2,15 +2,15 @@ version: '3' | ||||
| services: | ||||
|   redis: | ||||
|     image: redis/redis-stack-server:latest | ||||
|     container_name: redis-mumui | ||||
|     container_name: redis | ||||
|     networks: | ||||
|       - mumui_network | ||||
|     volumes: | ||||
|       - redis_data:/data | ||||
| 
 | ||||
|   pushpin: | ||||
|     image: pushpin:mumui | ||||
|     container_name: pushpin-mumui | ||||
|     image: pushpin:local | ||||
|     container_name: pushpin | ||||
|     networks: | ||||
|       - mumui_network | ||||
|     depends_on: | ||||
| @ -18,9 +18,11 @@ services: | ||||
| 
 | ||||
|   postgres: | ||||
|     image: postgres:latest | ||||
|     container_name: postgres-mumui | ||||
|     env_file: | ||||
|       - .env | ||||
|     container_name: postgres | ||||
|     environment: | ||||
|       POSTGRES_DB: mumui | ||||
|       POSTGRES_USER: test | ||||
|       POSTGRES_PASSWORD: test | ||||
|     networks: | ||||
|       - mumui_network | ||||
|     volumes: | ||||
| @ -29,8 +31,6 @@ services: | ||||
|   mumui: | ||||
|     image: mumui:local | ||||
|     container_name: mumui | ||||
|     env_file: | ||||
|       - .env | ||||
|     networks: | ||||
|       - mumui_network | ||||
|     volumes: | ||||
| @ -39,8 +39,8 @@ services: | ||||
|       - postgres | ||||
| 
 | ||||
|   nginx: | ||||
|     image: nginx:mumui | ||||
|     container_name: nginx-mumui | ||||
|     image: nginx:local | ||||
|     container_name: nginx | ||||
|     networks: | ||||
|       - mumui_network | ||||
|     volumes: | ||||
| @ -50,6 +50,7 @@ services: | ||||
|     depends_on: | ||||
|       - mumui | ||||
|    | ||||
| 
 | ||||
| networks: | ||||
|   mumui_network: | ||||
|     driver: bridge | ||||
|  | ||||
| @ -4,7 +4,7 @@ from celery import Celery | ||||
| 
 | ||||
| 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.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 | ||||
| https://docs.djangoproject.com/en/4.2/ref/settings/ | ||||
| """ | ||||
| import os | ||||
| 
 | ||||
| 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! | ||||
| 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! | ||||
| DEBUG = not IS_PROD | ||||
| DEBUG = True | ||||
| 
 | ||||
| ALLOWED_HOSTS = ["*"] if not IS_PROD else [HOST.lstrip("https://")] | ||||
| ALLOWED_HOSTS = ["*"] | ||||
| 
 | ||||
| 
 | ||||
| # Application definition | ||||
| @ -84,8 +80,8 @@ DATABASES = { | ||||
|     "default": { | ||||
|         "ENGINE": "django.db.backends.postgresql", | ||||
|         "NAME": "mumui", | ||||
|         "USER": os.getenv("POSTGRES_USER", "test"), | ||||
|         "PASSWORD": os.getenv("POSTGRES_PASSWORD", "test"), | ||||
|         "USER": "test", | ||||
|         "PASSWORD": "test", | ||||
|         "HOST": "postgres", | ||||
|         "PORT": "5432", | ||||
|     } | ||||
| @ -137,6 +133,6 @@ LOGIN_REDIRECT_URL = "home" | ||||
| LOGOUT_REDIRECT_URL = "home" | ||||
| 
 | ||||
| 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 = [ | ||||
|     path("admin/", admin.site.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"), | ||||
| ] | ||||
|  | ||||
| @ -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 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 | ||||
| @ -17,14 +17,14 @@ | ||||
| {% block content %} | ||||
|     <div class="container-fluid"> | ||||
|         <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 %} | ||||
|                     <table class="table table-striped table-hover"> | ||||
|                         <tr> | ||||
|                             <th scope="col-3">Name</th> | ||||
|                             <th scope="col-3">Type</th> | ||||
|                             <th scope="col-3">Status</th> | ||||
|                             <th class="col-1"></th> | ||||
|                             <th class="col-2"></th> | ||||
|                             <th class="col-2"></th> | ||||
|                         </tr> | ||||
|                         {% for deployment in page_obj %} | ||||
| @ -34,27 +34,21 @@ | ||||
|                                 <th name="status">{{ deployment.status }}</th> | ||||
|                                 <th> | ||||
|                                     <a href="{% url 'deployment-details' deployment.id %}"> | ||||
|                                         <button class="btn btn-primary btn-sm">Details</button> | ||||
|                                         <button class="btn btn-primary">Details</button> | ||||
|                                     </a> | ||||
|                                 </th> | ||||
|                                 {% if deployment.status == "FAILED" or deployment.status == "READY" %} | ||||
|                                     <th name="deploy"> | ||||
|                                         <form action="{% url 'deployment-launch' deployment.id %}?page={{ page_obj.number }}" method="post"> | ||||
|                                             {% csrf_token %} | ||||
|                                             <button class="btn btn-success btn-sm" type="submit">Deploy</button> | ||||
|                                             <button class="btn btn-success" type="submit">Deploy</button> | ||||
|                                         </form> | ||||
|                                     </th> | ||||
|                                 {% elif deployment.status == "RUNNING" %} | ||||
|                                     <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 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> | ||||
|                                             <span role="status">Deploy</span> | ||||
|                                         </button> | ||||
|                                     </th> | ||||
|                                 {% else %} | ||||
| @ -64,12 +58,9 @@ | ||||
|                         {% endfor %} | ||||
|                     </table> | ||||
|                     {% 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 | ||||
|                     </button>                   | ||||
|                     {% if not can_create %} | ||||
|                         <p style="font-style: italic;">You can't create new deployments, contact administrator for support.</p> | ||||
|                     {% endif %} | ||||
|                 {% else %} | ||||
|                     <h4 style="text-align: center;">Please log in !</h4> | ||||
|                 {% endif %} | ||||
| @ -79,9 +70,7 @@ | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block modal %} | ||||
|     {% if can_create %} | ||||
|     {% include 'deployment/create_modal.html' %} | ||||
|     {% endif %} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block script %} | ||||
|  | ||||
| @ -1,23 +1,7 @@ | ||||
| {% extends 'base.html' %} | ||||
| 
 | ||||
| {% load static %} | ||||
| 
 | ||||
| {% 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 %} | ||||
|     <div class="container-fluid"> | ||||
|         <div class="row justify-content-md-center"> | ||||
| @ -35,48 +19,21 @@ | ||||
|                         <input type="text" readonly class="form-control-plaintext" id="status" value="{{ deployment.status }}"> | ||||
|                         <label for="status">Status</label> | ||||
|                     </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"> | ||||
|                         <input type="text" readonly class="form-control-plaintext" id="created_at" value="{{ deployment.created_at }}"> | ||||
|                         <label for="created-at">Created at</label> | ||||
|                     </div> | ||||
|                     <div class="form-floating mb-3"> | ||||
|                         <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> | ||||
|                     {% if deployment.status == "RUNNING" %} | ||||
|                         <div class="form-floating mb-3"> | ||||
|                             <input type="text" readonly class="form-control-plaintext" id="updated_at" value="{{ deployment.task_id }}"> | ||||
|                             <label for="updated-at">Task UUID</label> | ||||
|                         </div> | ||||
|                     {% endif %} | ||||
|                     {% 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"> | ||||
|                     <form 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> | ||||
|                         {% if deployment.status != "RUNNING" %} | ||||
|                             <button type="submit" class="btn btn-danger">Delete</button> | ||||
|                         {% endif %} | ||||
|                     </form> | ||||
|                 {% else %} | ||||
|                     <h4 style="text-align: center;">Please log in !</h4> | ||||
|                 {% endif %} | ||||
| @ -86,13 +43,9 @@ | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block script %} | ||||
|     {% if deployment.status == "RUNNING" or deployment.status == "PENDING" %} | ||||
|         <script src="{% static 'deployment/js/event_source_details.js' %}" /> | ||||
|         </script> | ||||
|     {% endif %} | ||||
|     <script> | ||||
| <script> | ||||
|     function goBack() { | ||||
|             window.location={% url 'deployments' %}; | ||||
|         window.history.back(); | ||||
|     } | ||||
|     </script> | ||||
| </script> | ||||
| {% 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"> | ||||
|     <nav class="navbar navbar-expand-lg bg-dark border-bottom border-body" data-bs-theme="dark" style="width: 100%;"> | ||||
|         <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> | ||||
|             <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" | ||||
|                 aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"> | ||||
| @ -15,7 +12,7 @@ | ||||
|                         <a class="nav-link" href="/">Home</a> | ||||
|                     </li> | ||||
|                     <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 class="nav-item"> | ||||
|                         <a class="nav-link" href="#">About</a></li> | ||||
| @ -30,3 +27,34 @@ | ||||
|         </div> | ||||
|     </nav> | ||||
| </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="col-lg-6 col-md-8 col-sm-8"> | ||||
|                     <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 ! | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user