Compare commits

...

26 Commits

Author SHA1 Message Date
5b2110d6e0 update images tag 2024-07-12 09:17:47 +00:00
a62be9b5e7 change deployment url to /deployments 2023-09-26 13:58:14 +02:00
2b392d76d4 add a space in created deploying element 2023-09-26 13:39:10 +02:00
981555bcf3 add spinner when receiving running state + fix stream in details for pending state 2023-09-26 13:28:59 +02:00
57750c56d1 not call abort method 2023-09-26 12:32:31 +02:00
84f436817b load stream js scripts on pending state 2023-09-26 12:20:43 +02:00
d24e42ec8e update status on listening pending events 2023-09-26 12:15:59 +02:00
7d768127a6 push notifications when product is in running state 2023-09-26 11:26:31 +02:00
e9b98ae716 update README.md 2023-09-24 17:23:33 +02:00
bed711f46c add beautiful icon 2023-09-24 17:16:11 +02:00
ed4ea2eb62 fix frontend responsive issues 2023-09-24 16:46:37 +02:00
6c87655ee6 better with files... 2023-09-24 16:33:34 +02:00
33ac6f9ad6 rename channel module + update user creation + fix read channel permission 2023-09-24 16:33:08 +02:00
861d18484f return bad request on creation bad inputs 2023-09-24 15:44:30 +02:00
50963324f3 add deployment user to limit deployment 2023-09-24 15:35:22 +02:00
4e1a2ba20b fix events url + check on deployment manager + refactor send event 2023-09-23 14:27:08 +02:00
7030b95dc5 fix Makefile dev entries 2023-09-23 07:16:37 +02:00
f86fae86ef register deployment model in admin 2023-09-23 07:15:30 +02:00
5d68e355f8 send events on abort 2023-09-22 22:47:24 +02:00
89396ebac9 set environment settings 2023-09-22 22:17:21 +02:00
1aef5afe18 not format and lint on build 2023-09-22 19:42:50 +00:00
rmanach
aa0c15bd7a abort running task 2023-09-22 17:57:38 +02:00
rmanach
1d9c4e0164 add progress bar 2023-09-22 16:42:34 +02:00
rmanach
b9e87bd0ad add pyproject.toml 2023-09-21 15:04:05 +02:00
rmanach
e3845dbc8f fix details update title 2023-09-21 14:45:26 +02:00
rmanach
160d2faab6 fix go back button 2023-09-21 12:25:30 +02:00
28 changed files with 557 additions and 141 deletions

12
.env.example Normal file
View File

@ -0,0 +1,12 @@
# 'dev' or whatever for dev environment, otherwise 'prod' to enable production environment
ENV=dev
# used only on production environment
HOST=
POSTGRES_DB=mumui
POSTGRES_USER=mumui
POSTGRES_PASSWORD=admin
# used at installation
ADMIN_PASSWORD=admin

2
.gitignore vendored
View File

@ -5,3 +5,5 @@ venv
/static /static
*.log *.log
*.pid *.pid
docker-compose.override.yml
.env

View File

@ -1,12 +1,8 @@
# TODO(rmanach): add a pyproject.toml
format: format:
black deployment/*.py @./venv/bin/black mumui/*.py deployment/*.py
black mumui/*.py
lint: lint:
ruff deployment/*.py @./venv/bin/ruff .
ruff mumui/*.py
dev: dev:
rm -rf venv rm -rf venv
@ -18,10 +14,10 @@ django:
docker build . -t mumui:local docker build . -t mumui:local
pushpin-local: pushpin-local:
cd pushpin && docker build . -t pushpin:local cd pushpin && docker build . -t pushpin:mumui
nginx-local: nginx-local:
cd nginx && docker build . -t nginx:local cd nginx && docker build . -t nginx:mumui
build: build:
$(MAKE) pushpin-local $(MAKE) pushpin-local

View File

@ -6,6 +6,12 @@
Well, i had to choose a project name and i have no idea... I was listening this [sh*t](https://www.youtube.com/watch?v=8ZawzGgwIbQ) and then a sudden flash came to me: **mumu-i** (**i** for interface, to be more professional). Well, i had to choose a project name and i have no idea... I was listening this [sh*t](https://www.youtube.com/watch?v=8ZawzGgwIbQ) and then a sudden flash came to me: **mumu-i** (**i** for interface, to be more professional).
You can test the application here: [mumui.thegux.fr](https://mumui.thegux.fr).
* username: **demo**
* password: **demo1234**
Enjoy ! 😛
## Build ## Build
Some images to build: Some images to build:
@ -24,6 +30,4 @@ make run
Connect to [localhost:8080](http://localhost:8080), login as **admin** with password **admin** and start to play with deployments. Connect to [localhost:8080](http://localhost:8080), login as **admin** with password **admin** and start to play with deployments.
Enjoy ! 😛
PS: more explanations about the project and the stack are coming soon. I promise. 😘 PS: more explanations about the project and the stack are coming soon. I promise. 😘

View File

@ -1,3 +1,15 @@
# from django.contrib import admin from django.contrib import admin
# Register your models here. from deployment.models import Deployment, DeploymentUser
class DeploymentUserAdmin(admin.ModelAdmin):
pass
class DeploymentAdmin(admin.ModelAdmin):
pass
admin.site.register(Deployment, DeploymentAdmin)
admin.site.register(DeploymentUser, DeploymentUserAdmin)

View File

@ -4,3 +4,8 @@ from django.apps import AppConfig
class DeploymentConfig(AppConfig): class DeploymentConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "deployment" name = "deployment"
def ready(self) -> None:
import deployment.signals # noqa
return super().ready()

View File

@ -1,6 +0,0 @@
from django_eventstream.channelmanager import DefaultChannelManager
class DeploymentChannelManager(DefaultChannelManager):
def can_read_channel(self, user, channel):
return user is not None and user.is_authenticated

51
deployment/channels.py Normal file
View File

@ -0,0 +1,51 @@
from django.contrib.auth.models import User
from django_eventstream import send_event
from django_eventstream.channelmanager import DefaultChannelManager
from deployment.models import Deployment
def parse_channel(channel: str) -> tuple[str] | None:
parts = channel.lstrip("deployment_").split("_")
if len_part := len(parts):
if len_part == 1:
return (int(parts[0]), "")
if len_part == 2:
return (int(parts[0]), parts[1])
return
class DeploymentChannelManager(DefaultChannelManager):
def can_read_channel(self, user: User, channel: str):
if not user.has_perm("django_eventstream.view_event"):
return False
match parse_channel(channel):
case (user_id, ""):
return user_id == user.id or user.is_superuser
case (user_id, _):
# TODO(rmanach): check if the deployment belongs to the user
return user_id == user.id or user.is_superuser
return False
class Event:
@staticmethod
def send_details(deployment: Deployment, progress: int):
send_event(
f"deployment_{deployment.user.id}_{deployment.id}",
"message",
{
"id": deployment.id,
"status": deployment.status,
"progress": progress,
},
)
@staticmethod
def send(deployment: Deployment):
send_event(
f"deployment_{deployment.user.id}",
"message",
{"id": deployment.id, "status": deployment.status},
)

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.5 on 2023-09-20 17:23 # Generated by Django 4.2.5 on 2023-09-22 14:39
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -46,6 +46,8 @@ class Migration(migrations.Migration):
), ),
("created_at", models.DateTimeField(auto_now_add=True)), ("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)), ("updated_at", models.DateTimeField(auto_now=True)),
("error", models.TextField(null=True)),
("task_id", models.UUIDField(null=True)),
( (
"user", "user",
models.ForeignKey( models.ForeignKey(

View File

@ -0,0 +1,25 @@
# Generated by Django 4.2.5 on 2023-09-23 14:21
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('deployment', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='DeploymentUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('credits', models.SmallIntegerField(default=1, help_text='number of deployment allowed (-1 infinite)', validators=[django.core.validators.MinValueValidator(-1, message='Value must be greater than or equal to -1')])),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,8 +1,9 @@
from enum import Enum from enum import Enum
from datetime import datetime from typing import Any
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import MinValueValidator
from django.db import models from django.db import models
@ -28,6 +29,21 @@ class Status(Enum):
return [(s.name, s.value) for s in cls] return [(s.name, s.value) for s in cls]
class DeploymentUser(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
credits = models.SmallIntegerField(
null=False,
default=1,
help_text="number of deployment allowed (-1 infinite)",
validators=[
MinValueValidator(-1, message="Value must be greater than or equal to -1"),
],
)
def __str__(self) -> str:
return f"{self.user.username} - {self.credits}"
class Deployment(models.Model): class Deployment(models.Model):
id = models.UUIDField(primary_key=True) id = models.UUIDField(primary_key=True)
name = models.TextField(null=False, blank=False) name = models.TextField(null=False, blank=False)
@ -39,7 +55,13 @@ class Deployment(models.Model):
) )
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
error = models.TextField(null=True, blank=False)
task_id = models.UUIDField(null=True)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self): def __str__(self):
return f"{self.name} | {self.type} | {self.status}" return f"{self.name} | {self.type} | {self.status}"
def __init__(self, *args: Any, **kwargs: Any):
self.progress = None
super().__init__(*args, **kwargs)

27
deployment/signals.py Normal file
View File

@ -0,0 +1,27 @@
from django.contrib.auth.models import User, Permission
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from deployment.models import DeploymentUser
@receiver(post_save, sender=User)
def user_created_callback(sender, instance: User, created, **kwargs):
if created:
try:
with transaction.atomic():
if not instance.is_superuser:
permission = Permission.objects.get(
codename="view_event",
content_type__app_label="django_eventstream",
)
instance.user_permissions.add(permission)
permission.save()
user = DeploymentUser(
user=instance, credits=-1 if instance.is_superuser else 3
)
user.save()
except Exception as e:
print(f"deployment user creation failed: {e}")

View File

@ -1,7 +1,8 @@
var start = function (url) { var start = function (url) {
var es = new ReconnectingEventSource(url); var es = new ReconnectingEventSource(url);
console.log("url: " + url);
es.onopen = function () { es.onopen = function () {
console.log('connected'); console.log('connected');
}; };
@ -29,14 +30,34 @@ var start = function (url) {
status.innerHTML = message.status; status.innerHTML = message.status;
var button = tr.querySelector("th[name='deploy']"); var button = tr.querySelector("th[name='deploy']");
if (message.status == "RUNNING") {
var innerBtn = document.createElement("button");
innerBtn.setAttribute("disabled", "");
innerBtn.setAttribute("type", "button");
innerBtn.className = "btn btn-primary btn-sm"
var innerSpan = document.createElement("span");
innerSpan.setAttribute("role", "status");
innerSpan.innerHTML = " Deploying...";
var innerSpinner = document.createElement("span");
innerSpinner.className = "spinner-border spinner-border-sm";
innerSpinner.setAttribute("aria-hidden", "true");
innerBtn.appendChild(innerSpinner);
innerBtn.appendChild(innerSpan);
button.innerHTML = "";
button.appendChild(innerBtn);
}
if (message.status == "SUCCESS") { if (message.status == "SUCCESS") {
button.innerHTML = ""; button.innerHTML = "";
} }
if (message.status == "FAILED") { if (message.status == "FAILED") {
window.location.reload(); window.location.reload();
} }
} }
}, false); }, false);
}; };

View File

@ -0,0 +1,43 @@
var start = function (url) {
var es = new ReconnectingEventSource(url);
console.log("url: " + url);
es.onopen = function () {
console.log('connected');
};
es.addEventListener('stream-error', function (e) {
es.close();
message = JSON.parse(e.data);
console.log('stream error: ' + message.condition + ': ' + message.text);
}, false);
es.onerror = function (e) {
console.log('connection error');
};
es.addEventListener('message', function (e) {
message = JSON.parse(e.data);
console.log("id: " + message.id);
console.log("status: " + message.status);
console.log("progress: " + message.progress);
var status = document.getElementById("status");
status.setAttribute("value", message.status)
var progress = document.getElementById("deployment-progress");
// no progress in `PENDING` state
if (progress !== undefined) {
progress.style["width"] = message.progress+"%";
}
if (message.status == "SUCCESS") {
setTimeout(() => window.location.reload(), 1000);
}
if (message.status == "FAILED") {
window.location.reload();
}
}, false);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -3,30 +3,100 @@ import random
from uuid import UUID from uuid import UUID
from celery import shared_task from celery import shared_task
from django_eventstream import send_event from celery.contrib.abortable import AbortableTask
from deployment.channels import Event
from deployment.models import Deployment, Type, Status from deployment.models import Deployment, Type, Status
@shared_task class DeploymentTask(AbortableTask):
def deploy(deployment_id: UUID): def try_exec(self, stop: int):
deploy = Deployment.objects.get(id=deployment_id) if stop == 0:
stop = 1
deploy.status = Status.RUNNING.name for i in range(1, stop):
deploy.save() time.sleep(5 * stop - 2 * i)
match deploy.type: if random.randint(0, 10) == 10:
case Type.SLIM.name: raise Exception("unable to perform the deployment")
time.sleep(10)
case Type.MEDIUM.name:
time.sleep(60)
case Type.LARGE.name:
time.sleep(120)
deploy.status = ( yield i
Status.FAILED.name if random.randint(0, 10) % 2 != 0 else Status.SUCCESS.name
)
deploy.save()
send_event("deployment", "message", {"id": deploy.id, "status": deploy.status}) def run_slim(self):
print("event sent") progress = 95
self.update_state(meta={"progress": progress})
time.sleep(5)
status = Status.SUCCESS.name
progress = 100
self.update_state(meta={"progress": progress})
self.deploy.status = status
Event.send_details(self.deploy, progress)
def run_medium(self):
progress = 62
self.update_state(meta={"progress": progress})
time.sleep(10)
if random.randint(0, 10) % 2 != 0:
status = Status.FAILED.name
self.deploy.error = "arg.. no chance"
else:
status = Status.SUCCESS.name
progress = 100
self.update_state(meta={"progress": progress})
self.deploy.status = status
Event.send_details(self.deploy, progress)
def run_large(self):
progress = 0
try:
for i in self.try_exec(6):
progress = i * 20
self.update_state(meta={"progress": progress})
if i != 5:
Event.send_details(self.deploy, progress)
except Exception as e:
self.deploy.status = Status.FAILED.name
self.deploy.error = e
else:
self.deploy.status = Status.SUCCESS.name
Event.send_details(self.deploy, progress)
@property
def progress(self):
return self.request.get("meta", {}).get("progress", 0)
def launch(self, deployment_id: UUID):
progress = 0
self.update_state(meta={"progress": progress})
self.deploy = Deployment.objects.get(id=deployment_id)
self.deploy.task_id = self.request.id
self.deploy.status = Status.RUNNING.name
self.deploy.save()
Event.send(self.deploy)
Event.send_details(self.deploy, 0)
match self.deploy.type:
case Type.SLIM.name:
self.run_slim()
case Type.MEDIUM.name:
self.run_medium()
case Type.LARGE.name:
self.run_large()
self.deploy.task_id = None
self.deploy.save()
Event.send(self.deploy)
@shared_task(base=DeploymentTask, bind=True, ignore_result=True)
def deploy(self, deployment_id: UUID):
self.launch(deployment_id)

View File

@ -1,17 +1,23 @@
import django_eventstream import django_eventstream
from django.urls import path, include from django.urls import path, include
from deployment.views import index, create, details, deploy from deployment.views import index, create, details, deploy, abort
urlpatterns = [ urlpatterns = [
path("", index, name="deployment"), path("", index, name="deployments"),
path("create", create, name="deployment-create"), path("create", create, name="deployment-create"),
path("<uuid:deployment_id>", details, name="deployment-details"), path("<uuid:deployment_id>", details, name="deployment-details"),
path("<uuid:deployment_id>/deploy", deploy, name="deployment-launch"), path("<uuid:deployment_id>/deploy", deploy, name="deployment-launch"),
path("<uuid:deployment_id>/abort", abort, name="deployment-abort"),
path( path(
"events/", "events/<user_id>/",
include(django_eventstream.urls), include(django_eventstream.urls),
{"channels": ["deployment"]}, {"format-channels": ["deployment_{user_id}"]},
name="deployment-events", name="deployments-events",
),
path(
"events/<user_id>/<deployment_id>/",
include(django_eventstream.urls),
{"format-channels": ["deployment_{user_id}_{deployment_id}"]},
), ),
] ]

View File

@ -1,18 +1,39 @@
from uuid import uuid4 from uuid import uuid4
from celery.result import AsyncResult
from celery.contrib.abortable import AbortableAsyncResult
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import ( from django.http import (
HttpResponseRedirect, HttpResponseRedirect,
HttpResponseServerError, HttpResponseServerError,
HttpResponseBadRequest, HttpResponseBadRequest,
HttpResponseForbidden,
) )
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from deployment.channels import Event
from deployment.forms import DeploymentForm from deployment.forms import DeploymentForm
from deployment.models import Deployment, Status from deployment.models import Deployment, Status
from deployment.tasks import deploy as launch_deploy from deployment.tasks import deploy as launch_deploy
def check_user_credits(user: User) -> bool:
if not user.is_superuser:
try:
dep_user = user.deploymentuser
except (ObjectDoesNotExist, AttributeError):
return False
else:
if dep_user.credits < 0:
return True
deployments = Deployment.objects.filter(user=user)
return dep_user.credits > len(deployments)
return True
def index(request): def index(request):
deployments = Deployment.objects.filter(user=request.user.id).order_by( deployments = Deployment.objects.filter(user=request.user.id).order_by(
"-created_at" "-created_at"
@ -27,8 +48,9 @@ def index(request):
"deployment/board.html", "deployment/board.html",
{ {
"page_obj": page_obj, "page_obj": page_obj,
"range_pages": [i + 1 for i in range(page_obj.paginator.num_pages)], "range_pages": (i + 1 for i in range(page_obj.paginator.num_pages)),
"url": "events/", "url": f"events/{request.user.id}/",
"can_create": check_user_credits(request.user),
}, },
) )
@ -37,40 +59,90 @@ def deploy(request, deployment_id):
deployment = get_object_or_404(Deployment, id=deployment_id) deployment = get_object_or_404(Deployment, id=deployment_id)
if request.method == "POST": if request.method == "POST":
if deployment.status not in (Status.READY.name, Status.FAILED.name):
return HttpResponseBadRequest("deployment is undeployable")
# override previous errors
if deployment.error:
deployment.error = None
deployment.status = Status.PENDING.name deployment.status = Status.PENDING.name
deployment.save() deployment.save()
launch_deploy.delay(deployment_id) launch_deploy.delay(deployment_id)
if page := request.GET.get("page", ""): if page := request.GET.get("page", ""):
return HttpResponseRedirect(f"/deployment?page={page}") return HttpResponseRedirect(f"/deployments?page={page}")
return HttpResponseRedirect("/deployment") return HttpResponseRedirect("/deployments")
def abort(request, deployment_id):
deployment = get_object_or_404(Deployment, id=deployment_id)
if request.method == "POST":
if deployment.status not in (Status.RUNNING.name, Status.PENDING.name):
return HttpResponseBadRequest("deployment is unabortable")
res = AbortableAsyncResult(str(deployment.task_id))
progress = res.info.get("progress", 0)
res.revoke(terminate=True)
deployment.status = Status.FAILED.name
deployment.error = f"aborted by {request.user}"
deployment.task_id = None
deployment.save()
Event.send_details(deployment, progress)
Event.send(deployment)
return HttpResponseRedirect(f"/deployments/{deployment.id}")
def details(request, deployment_id): def details(request, deployment_id):
deployment = get_object_or_404(Deployment, id=deployment_id) deployment = get_object_or_404(Deployment, id=deployment_id)
if deployment.status == Status.RUNNING.name:
# retrieve the progression task in Redis
res = AsyncResult(str(deployment.task_id))
deployment.progress = res.info.get("progress", 0)
if request.method == "POST": if request.method == "POST":
if deployment.status == Status.RUNNING.name: if deployment.status == Status.RUNNING.name:
return HttpResponseBadRequest("deployment is running") return HttpResponseBadRequest("deployment is running")
if deployment.status == Status.PENDING.name:
return HttpResponseBadRequest("deployment is pending")
try: try:
deployment.delete() deployment.delete()
except Exception as e: except Exception as e:
return HttpResponseServerError(e) return HttpResponseServerError(e)
return HttpResponseRedirect("/deployment") return HttpResponseRedirect("/deployments")
return render(request, "deployment/details.html", {"deployment": deployment}) return render(
request,
"deployment/details.html",
{"deployment": deployment, "url": f"events/{request.user.id}/{deployment.id}/"},
)
def create(request): def create(request):
if request.method == "POST": if request.method == "POST":
form = DeploymentForm(request.POST) if not check_user_credits(request.user):
if form.is_valid(): return HttpResponseForbidden(
try: "unable to launch deployment, contact administrator for support"
Deployment.objects.create( )
user=request.user, id=uuid4(), **form.cleaned_data
)
except Exception as e:
return HttpResponseServerError(e)
return HttpResponseRedirect("/deployment") form = DeploymentForm(request.POST)
if not form.is_valid():
return HttpResponseBadRequest(
f"deployment creation inputs are invalid: {form.errors}"
)
try:
Deployment.objects.create(
user=request.user, id=uuid4(), **form.cleaned_data
)
except Exception as e:
return HttpResponseServerError(e)
return HttpResponseRedirect("/deployments")

View File

@ -2,15 +2,15 @@ version: '3'
services: services:
redis: redis:
image: redis/redis-stack-server:latest image: redis/redis-stack-server:latest
container_name: redis container_name: redis-mumui
networks: networks:
- mumui_network - mumui_network
volumes: volumes:
- redis_data:/data - redis_data:/data
pushpin: pushpin:
image: pushpin:local image: pushpin:mumui
container_name: pushpin container_name: pushpin-mumui
networks: networks:
- mumui_network - mumui_network
depends_on: depends_on:
@ -18,11 +18,9 @@ services:
postgres: postgres:
image: postgres:latest image: postgres:latest
container_name: postgres container_name: postgres-mumui
environment: env_file:
POSTGRES_DB: mumui - .env
POSTGRES_USER: test
POSTGRES_PASSWORD: test
networks: networks:
- mumui_network - mumui_network
volumes: volumes:
@ -31,6 +29,8 @@ services:
mumui: mumui:
image: mumui:local image: mumui:local
container_name: mumui container_name: mumui
env_file:
- .env
networks: networks:
- mumui_network - mumui_network
volumes: volumes:
@ -39,8 +39,8 @@ services:
- postgres - postgres
nginx: nginx:
image: nginx:local image: nginx:mumui
container_name: nginx container_name: nginx-mumui
networks: networks:
- mumui_network - mumui_network
volumes: volumes:
@ -50,7 +50,6 @@ services:
depends_on: depends_on:
- mumui - mumui
networks: networks:
mumui_network: mumui_network:
driver: bridge driver: bridge

View File

@ -4,7 +4,7 @@ from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mumui.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mumui.settings")
app = Celery("mumui", broker="redis://redis:6379/0") app = Celery("mumui", broker="redis://redis:6379/0", backend="redis://redis:6379/0")
app.config_from_object("django.conf:settings", namespace="CELERY") app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks() app.autodiscover_tasks()

View File

@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/ https://docs.djangoproject.com/en/4.2/ref/settings/
""" """
import os
from pathlib import Path from pathlib import Path
@ -22,10 +23,13 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-_c56%%c8%g%@5(3&thxi7ku2a&wst8lik*8@l0=#)ar)s86g36" SECRET_KEY = "django-insecure-_c56%%c8%g%@5(3&thxi7ku2a&wst8lik*8@l0=#)ar)s86g36"
# SECURITY WARNING: don't run with debug turned on in production! IS_PROD = os.getenv("ENV") == "prod"
DEBUG = True HOST = "*" if not IS_PROD else os.getenv("HOST", "*")
ALLOWED_HOSTS = ["*"] # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = not IS_PROD
ALLOWED_HOSTS = ["*"] if not IS_PROD else [HOST.lstrip("https://")]
# Application definition # Application definition
@ -80,8 +84,8 @@ DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql", "ENGINE": "django.db.backends.postgresql",
"NAME": "mumui", "NAME": "mumui",
"USER": "test", "USER": os.getenv("POSTGRES_USER", "test"),
"PASSWORD": "test", "PASSWORD": os.getenv("POSTGRES_PASSWORD", "test"),
"HOST": "postgres", "HOST": "postgres",
"PORT": "5432", "PORT": "5432",
} }
@ -133,6 +137,6 @@ LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home" LOGOUT_REDIRECT_URL = "home"
GRIP_URL = "http://pushpin:5561" GRIP_URL = "http://pushpin:5561"
EVENTSTREAM_CHANNELMANAGER_CLASS = "deployment.channel.DeploymentChannelManager" EVENTSTREAM_CHANNELMANAGER_CLASS = "deployment.channels.DeploymentChannelManager"
CSRF_TRUSTED_ORIGINS = ["http://localhost:8080"] CSRF_TRUSTED_ORIGINS = [HOST] if IS_PROD else ["http://localhost:8080"]

View File

@ -21,6 +21,6 @@ from django.views.generic.base import TemplateView
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")), path("accounts/", include("django.contrib.auth.urls")),
path("deployment/", include("deployment.urls")), path("deployments/", include("deployment.urls")),
path("", TemplateView.as_view(template_name="home.html"), name="home"), path("", TemplateView.as_view(template_name="home.html"), name="home"),
] ]

19
pyproject.toml Normal file
View File

@ -0,0 +1,19 @@
[tool.black]
line-length = 88
[tool.ruff]
select = ["E", "F"]
ignore = []
exclude = [
".git",
".ruff_cache",
"venv",
"settings.py",
"deployment/migrations/*.py"
]
line-length = 88
target-version = "py310"
[tool.ruff.mccabe]
max-complexity = 10

View File

@ -5,6 +5,6 @@ python manage.py makemigrations
python manage.py migrate python manage.py migrate
python manage.py collectstatic --no-input python manage.py collectstatic --no-input
DJANGO_SUPERUSER_PASSWORD=admin python manage.py createsuperuser --noinput --username admin --email admin@admin.fr DJANGO_SUPERUSER_PASSWORD=${ADMIN_PASSWORD} python manage.py createsuperuser --noinput --username admin --email admin@admin.fr
supervisord -c /app/supervisord.conf supervisord -c /app/supervisord.conf

View File

@ -17,14 +17,14 @@
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<div class="row justify-content-md-center"> <div class="row justify-content-md-center">
<div class="col-lg-8 col-md-8 col-sm-6"> <div class="col-lg-8 col-md-10 col-sm-12">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<tr> <tr>
<th scope="col-3">Name</th> <th scope="col-3">Name</th>
<th scope="col-3">Type</th> <th scope="col-3">Type</th>
<th scope="col-3">Status</th> <th scope="col-3">Status</th>
<th class="col-2"></th> <th class="col-1"></th>
<th class="col-2"></th> <th class="col-2"></th>
</tr> </tr>
{% for deployment in page_obj %} {% for deployment in page_obj %}
@ -34,21 +34,27 @@
<th name="status">{{ deployment.status }}</th> <th name="status">{{ deployment.status }}</th>
<th> <th>
<a href="{% url 'deployment-details' deployment.id %}"> <a href="{% url 'deployment-details' deployment.id %}">
<button class="btn btn-primary">Details</button> <button class="btn btn-primary btn-sm">Details</button>
</a> </a>
</th> </th>
{% if deployment.status == "FAILED" or deployment.status == "READY" %} {% if deployment.status == "FAILED" or deployment.status == "READY" %}
<th name="deploy"> <th name="deploy">
<form action="{% url 'deployment-launch' deployment.id %}?page={{ page_obj.number }}" method="post"> <form action="{% url 'deployment-launch' deployment.id %}?page={{ page_obj.number }}" method="post">
{% csrf_token %} {% csrf_token %}
<button class="btn btn-success" type="submit">Deploy</button> <button class="btn btn-success btn-sm" type="submit">Deploy</button>
</form> </form>
</th> </th>
{% elif deployment.status == "RUNNING" %} {% elif deployment.status == "RUNNING" %}
<th name="deploy"> <th name="deploy">
<button class="btn btn-primary" type="button" disabled> <button class="btn btn-primary btn-sm" type="button" disabled>
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
<span role="status">Deploy</span> <span role="status">Deploying...</span>
</button>
</th>
{% elif deployment.status == "PENDING" %}
<th name="deploy">
<button class="btn btn-primary btn-sm" type="button" disabled>
<span role="status">Pending...</span>
</button> </button>
</th> </th>
{% else %} {% else %}
@ -58,9 +64,12 @@
{% endfor %} {% endfor %}
</table> </table>
{% include 'pagination.html' %} {% include 'pagination.html' %}
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#create-deployment-modal"> <button type="button" {% if can_create %} class="btn btn-success" data-bs-toggle="modal" data-bs-target="#create-deployment-modal" {% else %} class="btn btn-danger" disabled {% endif %}>
Create Create
</button> </button>
{% if not can_create %}
<p style="font-style: italic;">You can't create new deployments, contact administrator for support.</p>
{% endif %}
{% else %} {% else %}
<h4 style="text-align: center;">Please log in !</h4> <h4 style="text-align: center;">Please log in !</h4>
{% endif %} {% endif %}
@ -70,7 +79,9 @@
{% endblock %} {% endblock %}
{% block modal %} {% block modal %}
{% include 'deployment/create_modal.html' %} {% if can_create %}
{% include 'deployment/create_modal.html' %}
{% endif %}
{% endblock %} {% endblock %}
{% block script %} {% block script %}

View File

@ -1,7 +1,23 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %}
{% block title %} Deployment details: {{ deployment.name }} {% endblock %} {% block title %} Deployment details: {{ deployment.name }} {% endblock %}
{% block bodyattr %}
{% if deployment.status == "RUNNING" or deployment.status == "PENDING" %}
onload="start('{{ url|safe }}');"
{% endif %}
{% endblock %}
{% block headscript %}
{% if deployment.status == "RUNNING" or deployment.status == "PENDING" %}
<script src="{% static 'django_eventstream/json2.js' %}"></script>
<script src="{% static 'django_eventstream/eventsource.min.js' %}"></script>
<script src="{% static 'django_eventstream/reconnecting-eventsource.js' %}"></script>
{% endif %}
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<div class="row justify-content-md-center"> <div class="row justify-content-md-center">
@ -19,21 +35,48 @@
<input type="text" readonly class="form-control-plaintext" id="status" value="{{ deployment.status }}"> <input type="text" readonly class="form-control-plaintext" id="status" value="{{ deployment.status }}">
<label for="status">Status</label> <label for="status">Status</label>
</div> </div>
{% if deployment.status == "RUNNING" or deployment.status == "PENDING" %}
<div class="form-floating mb-3">
<div class="progress" role="progressbar" aria-label="deployment-progress" aria-valuenow="{{ deployment.progress }}" aria-valuemin="0" aria-valuemax="100">
<div id="deployment-progress" class="progress-bar progress-bar-striped progress-bar-animated" style="width: {{ deployment.progress }}%"></div>
</div>
</div>
{% endif %}
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input type="text" readonly class="form-control-plaintext" id="created_at" value="{{ deployment.created_at }}"> <input type="text" readonly class="form-control-plaintext" id="created_at" value="{{ deployment.created_at }}">
<label for="created-at">Created at</label> <label for="created-at">Created at</label>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input type="text" readonly class="form-control-plaintext" id="updated_at" value="{{ deployment.updated_at }}"> <input type="text" readonly class="form-control-plaintext" id="updated_at" value="{{ deployment.updated_at }}">
<label for="updated-at">{{ deployment.updated_at }}</label> <label for="updated-at">Updated at</label>
</div> </div>
<form action="" method="post"> {% if deployment.status == "RUNNING" %}
{% csrf_token %} <div class="form-floating mb-3">
<button type="button" onclick="goBack()" class="btn btn-secondary">Back</button> <input type="text" readonly class="form-control-plaintext" id="updated_at" value="{{ deployment.task_id }}">
{% if deployment.status != "RUNNING" %} <label for="updated-at">Task UUID</label>
<button type="submit" class="btn btn-danger">Delete</button> </div>
{% endif %} {% endif %}
</form> {% if deployment.status == "FAILED" %}
<div class="form-floating mb-3">
<input type="text" readonly class="form-control-plaintext" id="updated_at" value="{{ deployment.error }}">
<label for="updated-at">Error</label>
</div>
{% endif %}
{% if deployment.status != "RUNNING" and deployment.status != "PENDING" %}
<form id="delete-deployment" action="" method="post">
{% csrf_token %}
</form>
{% else %}
<form id="abort-deployment" action="{% url 'deployment-abort' deployment.id %}" method="post">
{% csrf_token %}
</form>
{% endif %}
<button type="button" onclick="goBack()" class="btn btn-secondary">Back</button>
{% if deployment.status != "RUNNING" and deployment.status != "PENDING" %}
<button form="delete-deployment" type="submit" class="btn btn-danger">Delete</button>
{% else %}
<button form="abort-deployment" type="submit" class="btn btn-danger">Abort</button>
{% endif %}
{% else %} {% else %}
<h4 style="text-align: center;">Please log in !</h4> <h4 style="text-align: center;">Please log in !</h4>
{% endif %} {% endif %}
@ -43,9 +86,13 @@
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script> {% if deployment.status == "RUNNING" or deployment.status == "PENDING" %}
function goBack() { <script src="{% static 'deployment/js/event_source_details.js' %}" />
window.history.back(); </script>
} {% endif %}
</script> <script>
function goBack() {
window.location={% url 'deployments' %};
}
</script>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,9 @@
{% load static %}
<header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between mb-4 border-bottom"> <header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between mb-4 border-bottom">
<nav class="navbar navbar-expand-lg bg-dark border-bottom border-body" data-bs-theme="dark" style="width: 100%;"> <nav class="navbar navbar-expand-lg bg-dark border-bottom border-body" data-bs-theme="dark" style="width: 100%;">
<div class="container-fluid"> <div class="container-fluid">
<img src="{% static 'deployment/png/flash-icon.png' %}" alt="Logo" width="40" height="40" class="d-inline-block align-text-top">
<a class="navbar-brand" href="/">Mumui</a> <a class="navbar-brand" href="/">Mumui</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar"
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"> aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
@ -12,7 +15,7 @@
<a class="nav-link" href="/">Home</a> <a class="nav-link" href="/">Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="{% url 'deployment' %}" class="nav-link">Deployments</a></li> <a href="{% url 'deployments' %}" class="nav-link">Deployments</a></li>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#">About</a></li> <a class="nav-link" href="#">About</a></li>
@ -27,34 +30,3 @@
</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> -->

View File

@ -13,7 +13,7 @@
<div class="row justify-content-md-center"> <div class="row justify-content-md-center">
<div class="col-lg-6 col-md-8 col-sm-8"> <div class="col-lg-6 col-md-8 col-sm-8">
<div style="text-align: center;">Forget, i don't care... Please go on <div style="text-align: center;">Forget, i don't care... Please go on
<a href="/deployment/">Deployments</a> <a href="{% url 'deployments' %}">Deployments</a>
and enjoy the life ! and enjoy the life !
</div> </div>
</div> </div>