Compare commits
No commits in common. "main" and "feat/ui-upgrade" have entirely different histories.
main
...
feat/ui-up
@ -1,20 +0,0 @@
|
||||
.ruff_cache
|
||||
__pycache__
|
||||
|
||||
static
|
||||
|
||||
README.md
|
||||
Makefile
|
||||
|
||||
.env
|
||||
.env.example
|
||||
|
||||
venv
|
||||
|
||||
*.log
|
||||
*.pid
|
||||
|
||||
pushpin
|
||||
nginx
|
||||
|
||||
.deploy
|
||||
14
.env.example
14
.env.example
@ -1,14 +0,0 @@
|
||||
SECRET_KEY=
|
||||
|
||||
# 'dev' or whatever for dev environment, otherwise 'prod' to enable production environment
|
||||
ENV=dev
|
||||
|
||||
# used only on production environment
|
||||
HOSTS=
|
||||
|
||||
POSTGRES_DB=mumui
|
||||
POSTGRES_USER=mumui
|
||||
POSTGRES_PASSWORD=admin
|
||||
|
||||
# used at installation
|
||||
ADMIN_PASSWORD=admin
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,13 +1,7 @@
|
||||
.ruff_cache
|
||||
__pycache__
|
||||
|
||||
db.sqlite3
|
||||
venv
|
||||
|
||||
static
|
||||
|
||||
/static
|
||||
*.log
|
||||
*.pid
|
||||
|
||||
.env
|
||||
|
||||
.deploy
|
||||
@ -8,9 +8,12 @@ WORKDIR /app
|
||||
|
||||
RUN apt update && apt install -y
|
||||
|
||||
COPY . /app/
|
||||
COPY startup.sh /app/
|
||||
COPY requirements.txt /app/
|
||||
|
||||
RUN pip install -U pip
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
ENTRYPOINT [ "/app/startup.sh" ]
|
||||
|
||||
EXPOSE 8000
|
||||
30
Makefile
30
Makefile
@ -1,39 +1,35 @@
|
||||
-include .deploy/Makefile
|
||||
|
||||
PYTHON = venv/bin/python
|
||||
|
||||
VERSION := 0.1.0
|
||||
# TODO(rmanach): add a pyproject.toml
|
||||
|
||||
format:
|
||||
@$(PYTHON) -m black mumui/*.py deployment/*.py
|
||||
black deployment/*.py
|
||||
black mumui/*.py
|
||||
|
||||
lint:
|
||||
@$(PYTHON) -m ruff .
|
||||
ruff deployment/*.py
|
||||
ruff mumui/*.py
|
||||
|
||||
dev:
|
||||
rm -rf venv
|
||||
python3 -m virtualenv venv
|
||||
$(PYTHON) -m pip install -r requirements.txt
|
||||
$(PYTHON) -m pip install -r requirements-dev.txt
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
./venv/bin/pip install -r requirements-dev.txt
|
||||
|
||||
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
|
||||
$(MAKE) nginx-local
|
||||
$(MAKE) django
|
||||
|
||||
.PHONY: static
|
||||
static:
|
||||
SECRET_KEY="" $(PYTHON) manage.py collectstatic --no-input
|
||||
|
||||
run: pushpin-local nginx-local django
|
||||
@docker compose up
|
||||
run:
|
||||
docker compose up
|
||||
|
||||
stop:
|
||||
docker compose down
|
||||
@ -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.sonak.fr](https://mumui.sonak.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,13 +1,6 @@
|
||||
var es = null;
|
||||
|
||||
var start = function (url) {
|
||||
if (es) {
|
||||
console.log("closing es", es);
|
||||
es.close();
|
||||
}
|
||||
es = new ReconnectingEventSource(url);
|
||||
|
||||
console.log("url: " + url);
|
||||
var es = new ReconnectingEventSource(url);
|
||||
|
||||
es.onopen = function () {
|
||||
console.log('connected');
|
||||
@ -36,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 = "";
|
||||
}
|
||||
@ -65,19 +37,6 @@ var start = function (url) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
}, false);
|
||||
};
|
||||
|
||||
document.querySelectorAll(".btn-details").forEach(btn => {
|
||||
btn.addEventListener("click", function(e) {
|
||||
const durl = e.currentTarget.getAttribute("durl");
|
||||
console.log("details url:", durl);
|
||||
|
||||
if (es) {
|
||||
window.es.close()
|
||||
console.log("closing sse....");
|
||||
};
|
||||
|
||||
window.location = durl;
|
||||
});
|
||||
});
|
||||
@ -1,49 +0,0 @@
|
||||
var start = function (url) {
|
||||
if (window.es) {
|
||||
console.log("closing es", window.es);
|
||||
window.es.close();
|
||||
}
|
||||
|
||||
es = new ReconnectingEventSource(url);
|
||||
window.es = es;
|
||||
|
||||
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
|
||||
|
||||
@ -21,18 +20,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ["SECRET_KEY"]
|
||||
|
||||
IS_PROD = os.getenv("ENV") == "prod"
|
||||
|
||||
HOSTS = ["*"]
|
||||
if IS_PROD:
|
||||
HOSTS = os.environ["HOSTS"].split(",")
|
||||
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!
|
||||
DEBUG = not IS_PROD
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = [h.lstrip("https://") for h in HOSTS]
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
|
||||
# Application definition
|
||||
@ -87,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",
|
||||
}
|
||||
@ -140,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 = HOSTS 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,3 +1,3 @@
|
||||
FROM fanout/pushpin:latest
|
||||
|
||||
RUN echo "* api:8000" > /etc/pushpin/routes
|
||||
RUN echo "* mumui:8000" > /etc/pushpin/routes
|
||||
@ -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
|
||||
@ -3,7 +3,8 @@ pip install -r requirements.txt
|
||||
|
||||
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
|
||||
@ -4,17 +4,27 @@
|
||||
|
||||
{% block title %} Deployments board {% endblock %}
|
||||
|
||||
{% block headscript %}
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
||||
{% block bodyattr %}
|
||||
onload="start('{{ url|safe }}');"
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
@ -23,26 +33,22 @@
|
||||
<th name="type">{{ deployment.type }}</th>
|
||||
<th name="status">{{ deployment.status }}</th>
|
||||
<th>
|
||||
<button class="btn btn-primary btn-sm btn btn-details" durl="{% url 'deployment-details' deployment.id %}">Details</button>
|
||||
<a href="{% url 'deployment-details' deployment.id %}">
|
||||
<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 %}
|
||||
@ -52,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 %}
|
||||
@ -67,16 +70,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block modal %}
|
||||
{% if can_create %}
|
||||
{% include 'deployment/create_modal.html' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<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>
|
||||
<script src="{% static 'deployment/js/event_source.js' %}" />
|
||||
</script>
|
||||
<script>start('{{ url|safe }}');</script>
|
||||
{% endblock %}
|
||||
@ -1,7 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Deployment details: {{ deployment.name }} {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -21,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 id="btn-back" durl="{% url 'deployments' %}" type="button" 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>
|
||||
<button type="button" onclick="goBack()" class="btn btn-secondary">Back</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 %}
|
||||
@ -72,24 +43,9 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
{% 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>
|
||||
<script src="{% static 'deployment/js/event_source_details.js' %}" /></script>
|
||||
<script>start('{{ url|safe }}');</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
document.querySelector("#btn-back").addEventListener("click", function(e) {
|
||||
const durl = e.currentTarget.getAttribute("durl");
|
||||
console.log("deployment url:", durl);
|
||||
|
||||
if (window.es) {
|
||||
window.es.close()
|
||||
console.log("closing sse....");
|
||||
};
|
||||
|
||||
window.location = durl;
|
||||
});
|
||||
function goBack() {
|
||||
window.history.back();
|
||||
}
|
||||
</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