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
*.log
*.pid
docker-compose.override.yml
.env

View File

@ -1,12 +1,8 @@
# TODO(rmanach): add a pyproject.toml
format:
black deployment/*.py
black mumui/*.py
@./venv/bin/black mumui/*.py deployment/*.py
lint:
ruff deployment/*.py
ruff mumui/*.py
@./venv/bin/ruff .
dev:
rm -rf venv
@ -18,10 +14,10 @@ django:
docker build . -t mumui:local
pushpin-local:
cd pushpin && docker build . -t pushpin:local
cd pushpin && docker build . -t pushpin:mumui
nginx-local:
cd nginx && docker build . -t nginx:local
cd nginx && docker build . -t nginx:mumui
build:
$(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).
You can test the application here: [mumui.thegux.fr](https://mumui.thegux.fr).
* username: **demo**
* password: **demo1234**
Enjoy ! 😛
## 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.
Enjoy ! 😛
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):
default_auto_field = "django.db.models.BigAutoField"
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.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(

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 datetime import datetime
from typing import Any
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator
from django.db import models
@ -28,6 +29,21 @@ 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)
@ -39,7 +55,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)

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 es = new ReconnectingEventSource(url);
console.log("url: " + url);
es.onopen = function () {
console.log('connected');
};
@ -29,14 +30,34 @@ 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 = "";
}
if (message.status == "FAILED") {
window.location.reload();
window.location.reload();
}
}
}, 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 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
@shared_task
def deploy(deployment_id: UUID):
deploy = Deployment.objects.get(id=deployment_id)
class DeploymentTask(AbortableTask):
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
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
from django.urls import path, include
from deployment.views import index, create, details, deploy
from deployment.views import index, create, details, deploy, abort
urlpatterns = [
path("", index, name="deployment"),
path("", index, name="deployments"),
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/",
"events/<user_id>/",
include(django_eventstream.urls),
{"channels": ["deployment"]},
name="deployment-events",
{"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}"]},
),
]

View File

@ -1,18 +1,39 @@
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"
@ -27,8 +48,9 @@ def index(request):
"deployment/board.html",
{
"page_obj": page_obj,
"range_pages": [i + 1 for i in range(page_obj.paginator.num_pages)],
"url": "events/",
"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),
},
)
@ -37,40 +59,90 @@ 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"/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):
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("/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):
if request.method == "POST":
form = DeploymentForm(request.POST)
if form.is_valid():
try:
Deployment.objects.create(
user=request.user, id=uuid4(), **form.cleaned_data
)
except Exception as e:
return HttpResponseServerError(e)
if not check_user_credits(request.user):
return HttpResponseForbidden(
"unable to launch deployment, contact administrator for support"
)
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:
redis:
image: redis/redis-stack-server:latest
container_name: redis
container_name: redis-mumui
networks:
- mumui_network
volumes:
- redis_data:/data
pushpin:
image: pushpin:local
container_name: pushpin
image: pushpin:mumui
container_name: pushpin-mumui
networks:
- mumui_network
depends_on:
@ -18,11 +18,9 @@ services:
postgres:
image: postgres:latest
container_name: postgres
environment:
POSTGRES_DB: mumui
POSTGRES_USER: test
POSTGRES_PASSWORD: test
container_name: postgres-mumui
env_file:
- .env
networks:
- mumui_network
volumes:
@ -31,6 +29,8 @@ services:
mumui:
image: mumui:local
container_name: mumui
env_file:
- .env
networks:
- mumui_network
volumes:
@ -39,8 +39,8 @@ services:
- postgres
nginx:
image: nginx:local
container_name: nginx
image: nginx:mumui
container_name: nginx-mumui
networks:
- mumui_network
volumes:
@ -50,7 +50,6 @@ services:
depends_on:
- mumui
networks:
mumui_network:
driver: bridge

View File

@ -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()

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
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import os
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!
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 = True
IS_PROD = os.getenv("ENV") == "prod"
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
@ -80,8 +84,8 @@ DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "mumui",
"USER": "test",
"PASSWORD": "test",
"USER": os.getenv("POSTGRES_USER", "test"),
"PASSWORD": os.getenv("POSTGRES_PASSWORD", "test"),
"HOST": "postgres",
"PORT": "5432",
}
@ -133,6 +137,6 @@ LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"
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 = [
path("admin/", admin.site.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"),
]

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 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

View File

@ -17,14 +17,14 @@
{% block content %}
<div class="container-fluid">
<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 %}
<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-2"></th>
<th class="col-1"></th>
<th class="col-2"></th>
</tr>
{% for deployment in page_obj %}
@ -34,21 +34,27 @@
<th name="status">{{ deployment.status }}</th>
<th>
<a href="{% url 'deployment-details' deployment.id %}">
<button class="btn btn-primary">Details</button>
<button class="btn btn-primary btn-sm">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" type="submit">Deploy</button>
<button class="btn btn-success btn-sm" type="submit">Deploy</button>
</form>
</th>
{% elif deployment.status == "RUNNING" %}
<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 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>
</th>
{% else %}
@ -58,9 +64,12 @@
{% endfor %}
</table>
{% 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
</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 %}
@ -70,7 +79,9 @@
{% endblock %}
{% block modal %}
{% include 'deployment/create_modal.html' %}
{% if can_create %}
{% include 'deployment/create_modal.html' %}
{% endif %}
{% endblock %}
{% block script %}

View File

@ -1,7 +1,23 @@
{% 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">
@ -19,21 +35,48 @@
<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">{{ deployment.updated_at }}</label>
<label for="updated-at">Updated at</label>
</div>
<form action="" method="post">
{% csrf_token %}
<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>
{% 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">
{% 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 %}
<h4 style="text-align: center;">Please log in !</h4>
{% endif %}
@ -43,9 +86,13 @@
{% endblock %}
{% block script %}
<script>
function goBack() {
window.history.back();
}
</script>
{% if deployment.status == "RUNNING" or deployment.status == "PENDING" %}
<script src="{% static 'deployment/js/event_source_details.js' %}" />
</script>
{% endif %}
<script>
function goBack() {
window.location={% url 'deployments' %};
}
</script>
{% 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">
<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">
@ -12,7 +15,7 @@
<a class="nav-link" href="/">Home</a>
</li>
<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 class="nav-item">
<a class="nav-link" href="#">About</a></li>
@ -27,34 +30,3 @@
</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> -->

View File

@ -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="/deployment/">Deployments</a>
<a href="{% url 'deployments' %}">Deployments</a>
and enjoy the life !
</div>
</div>