diff --git a/deployment/migrations/0001_initial.py b/deployment/migrations/0001_initial.py index e2dae3c..35ae33b 100644 --- a/deployment/migrations/0001_initial.py +++ b/deployment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-09-20 17:23 +# Generated by Django 4.2.5 on 2023-09-22 14:39 from django.conf import settings from django.db import migrations, models @@ -46,6 +46,8 @@ 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( diff --git a/deployment/models.py b/deployment/models.py index a9a4698..ec0e511 100644 --- a/deployment/models.py +++ b/deployment/models.py @@ -1,4 +1,5 @@ from enum import Enum +from typing import Any from django.contrib.auth.models import User @@ -38,7 +39,13 @@ 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) diff --git a/deployment/static/deployment/js/event_source.js b/deployment/static/deployment/js/event_source.js index f940b19..7bfebab 100644 --- a/deployment/static/deployment/js/event_source.js +++ b/deployment/static/deployment/js/event_source.js @@ -1,6 +1,8 @@ var start = function (url) { var es = new ReconnectingEventSource(url); + console.log("url: " + url); + es.onopen = function () { console.log('connected'); }; @@ -36,6 +38,5 @@ var start = function (url) { window.location.reload(); } } - }, false); }; \ No newline at end of file diff --git a/deployment/static/deployment/js/event_source_details.js b/deployment/static/deployment/js/event_source_details.js new file mode 100644 index 0000000..f22bbf5 --- /dev/null +++ b/deployment/static/deployment/js/event_source_details.js @@ -0,0 +1,37 @@ +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 progress = document.getElementById("deployment-progress"); + progress.style["width"] = message.progress+"%"; + + if (message.status == "SUCCESS") { + setTimeout(() => window.location.reload(), 1000); + } + + if (message.status == "FAILED") { + window.location.reload(); + } + }, false); +}; \ No newline at end of file diff --git a/deployment/tasks.py b/deployment/tasks.py index d9b471b..47b193d 100644 --- a/deployment/tasks.py +++ b/deployment/tasks.py @@ -2,31 +2,135 @@ import time import random from uuid import UUID -from celery import shared_task +from celery import Task, shared_task from django_eventstream import send_event from deployment.models import Deployment, Type, Status -@shared_task -def deploy(deployment_id: UUID): - deploy = Deployment.objects.get(id=deployment_id) +class DeploymentTask(Task): + def try_exec(self, stop: int): + if stop == 0: + stop = 1 - deploy.status = Status.RUNNING.name - deploy.save() + for i in range(1, stop): + time.sleep(5 * stop - 2 * i) - match deploy.type: - case Type.SLIM.name: - time.sleep(10) - case Type.MEDIUM.name: - time.sleep(60) - case Type.LARGE.name: - time.sleep(120) + if random.randint(0, 10) == 10: + raise Exception("unable to perform the deployment") - deploy.status = ( - Status.FAILED.name if random.randint(0, 10) % 2 != 0 else Status.SUCCESS.name - ) - deploy.save() + yield i - send_event("deployment", "message", {"id": deploy.id, "status": deploy.status}) - print("event sent") + 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 + send_event( + f"deployment-{self.deploy.id}", + "message", + { + "id": self.deploy.id, + "status": self.deploy.status, + "progress": 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 + send_event( + f"deployment-{self.deploy.id}", + "message", + { + "id": self.deploy.id, + "status": self.deploy.status, + "progress": 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 progress != 100: + send_event( + f"deployment-{self.deploy.id}", + "message", + { + "id": self.deploy.id, + "status": self.deploy.status + if i != 5 + else Status.SUCCESS.name, + "progress": progress, + }, + ) + except Exception as e: + self.deploy.status = Status.FAILED.name + self.deploy.error = e + else: + self.deploy.status = Status.SUCCESS.name + + send_event( + f"deployment-{self.deploy.id}", + "message", + { + "id": self.deploy.id, + "status": self.deploy.status, + "progress": 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() + + 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() + + send_event( + "deployment", + "message", + {"id": self.deploy.id, "status": self.deploy.status}, + ) + + +@shared_task(base=DeploymentTask, bind=True, ignore_result=True) +def deploy(self, deployment_id: UUID): + self.launch(deployment_id) diff --git a/deployment/urls.py b/deployment/urls.py index f70f92a..882cf5f 100644 --- a/deployment/urls.py +++ b/deployment/urls.py @@ -14,4 +14,9 @@ urlpatterns = [ {"channels": ["deployment"]}, name="deployment-events", ), + path( + "/events/", + include(django_eventstream.urls), + {"format-channels": ["deployment-{deployment_id}"]}, + ), ] diff --git a/deployment/views.py b/deployment/views.py index 6a53aa7..b0722bc 100644 --- a/deployment/views.py +++ b/deployment/views.py @@ -1,5 +1,6 @@ from uuid import uuid4 +from celery.result import AsyncResult from django.core.paginator import Paginator from django.http import ( HttpResponseRedirect, @@ -37,6 +38,10 @@ def deploy(request, deployment_id): deployment = get_object_or_404(Deployment, id=deployment_id) if request.method == "POST": + # override previous errors + if deployment.error: + deployment.error = None + deployment.status = Status.PENDING.name deployment.save() launch_deploy.delay(deployment_id) @@ -49,17 +54,29 @@ def deploy(request, deployment_id): def details(request, deployment_id): deployment = get_object_or_404(Deployment, id=deployment_id) + if deployment.status == Status.RUNNING.name: + # retrieve the progression in the backend task + 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("/deployment") - return render(request, "deployment/details.html", {"deployment": deployment}) + return render( + request, + "deployment/details.html", + {"deployment": deployment, "url": f"{deployment.id}/events/"}, + ) def create(request): diff --git a/mumui/celery.py b/mumui/celery.py index 968cc5a..3c67ef4 100644 --- a/mumui/celery.py +++ b/mumui/celery.py @@ -4,7 +4,7 @@ from celery import Celery os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mumui.settings") -app = Celery("mumui", broker="redis://redis:6379/0") +app = Celery("mumui", broker="redis://redis:6379/0", backend="redis://redis:6379/0") app.config_from_object("django.conf:settings", namespace="CELERY") app.autodiscover_tasks() diff --git a/templates/deployment/details.html b/templates/deployment/details.html index 98c37a0..8822197 100644 --- a/templates/deployment/details.html +++ b/templates/deployment/details.html @@ -1,7 +1,27 @@ {% extends 'base.html' %} +{% load static %} + {% block title %} Deployment details: {{ deployment.name }} {% endblock %} +{% if deployment.status == "RUNNING" %} + + {% block bodyattr %} + {% if deployment.status == "RUNNING" %} + onload="start('{{ url|safe }}');" + {% endif %} + {% endblock %} + + {% block headscript %} + {% if deployment.status == "RUNNING" %} + + + + {% endif %} + {% endblock %} + +{% endif %} + {% block content %}
@@ -19,6 +39,13 @@
+ {% if deployment.status == "RUNNING" %} +
+
+
+
+
+ {% endif %}
@@ -27,6 +54,18 @@
+ {% if deployment.status == "RUNNING" %} +
+ + +
+ {% endif %} + {% if deployment.status == "FAILED" %} +
+ + +
+ {% endif %}
{% csrf_token %} @@ -43,9 +82,13 @@ {% endblock %} {% block script %} - + {% if deployment.status == "RUNNING" %} + + {% endif %} + {% endblock %} \ No newline at end of file