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
|
.ruff_cache
|
||||||
__pycache__
|
__pycache__
|
||||||
|
db.sqlite3
|
||||||
venv
|
venv
|
||||||
|
/static
|
||||||
static
|
|
||||||
|
|
||||||
*.log
|
*.log
|
||||||
*.pid
|
*.pid
|
||||||
|
|
||||||
.env
|
|
||||||
|
|
||||||
.deploy
|
|
||||||
@ -8,9 +8,12 @@ WORKDIR /app
|
|||||||
|
|
||||||
RUN apt update && apt install -y
|
RUN apt update && apt install -y
|
||||||
|
|
||||||
COPY . /app/
|
COPY startup.sh /app/
|
||||||
|
COPY requirements.txt /app/
|
||||||
|
|
||||||
RUN pip install -U pip
|
RUN pip install -U pip
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
ENTRYPOINT [ "/app/startup.sh" ]
|
ENTRYPOINT [ "/app/startup.sh" ]
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
30
Makefile
30
Makefile
@ -1,39 +1,35 @@
|
|||||||
-include .deploy/Makefile
|
# TODO(rmanach): add a pyproject.toml
|
||||||
|
|
||||||
PYTHON = venv/bin/python
|
|
||||||
|
|
||||||
VERSION := 0.1.0
|
|
||||||
|
|
||||||
format:
|
format:
|
||||||
@$(PYTHON) -m black mumui/*.py deployment/*.py
|
black deployment/*.py
|
||||||
|
black mumui/*.py
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@$(PYTHON) -m ruff .
|
ruff deployment/*.py
|
||||||
|
ruff mumui/*.py
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
rm -rf venv
|
rm -rf venv
|
||||||
python3 -m virtualenv venv
|
python3 -m virtualenv venv
|
||||||
$(PYTHON) -m pip install -r requirements.txt
|
./venv/bin/pip install -r requirements.txt
|
||||||
$(PYTHON) -m pip install -r requirements-dev.txt
|
./venv/bin/pip install -r requirements-dev.txt
|
||||||
|
|
||||||
django:
|
django:
|
||||||
docker build . -t mumui:local
|
docker build . -t mumui:local
|
||||||
|
|
||||||
pushpin-local:
|
pushpin-local:
|
||||||
cd pushpin && docker build . -t pushpin:mumui
|
cd pushpin && docker build . -t pushpin:local
|
||||||
|
|
||||||
nginx-local:
|
nginx-local:
|
||||||
cd nginx && docker build . -t nginx:mumui
|
cd nginx && docker build . -t nginx:local
|
||||||
|
|
||||||
build:
|
build:
|
||||||
$(MAKE) pushpin-local
|
$(MAKE) pushpin-local
|
||||||
$(MAKE) nginx-local
|
$(MAKE) nginx-local
|
||||||
$(MAKE) django
|
$(MAKE) django
|
||||||
|
|
||||||
.PHONY: static
|
run:
|
||||||
static:
|
docker compose up
|
||||||
SECRET_KEY="" $(PYTHON) manage.py collectstatic --no-input
|
|
||||||
|
|
||||||
run: pushpin-local nginx-local django
|
|
||||||
@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).
|
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
|
## Build
|
||||||
Some images to 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.
|
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. 😘
|
||||||
@ -1,15 +1,3 @@
|
|||||||
from django.contrib import admin
|
# from django.contrib import admin
|
||||||
|
|
||||||
from deployment.models import Deployment, DeploymentUser
|
# Register your models here.
|
||||||
|
|
||||||
|
|
||||||
class DeploymentUserAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DeploymentAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Deployment, DeploymentAdmin)
|
|
||||||
admin.site.register(DeploymentUser, DeploymentUserAdmin)
|
|
||||||
|
|||||||
@ -4,8 +4,3 @@ 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()
|
|
||||||
|
|||||||
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.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@ -46,8 +46,6 @@ 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(
|
||||||
|
|||||||
@ -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 enum import Enum
|
||||||
from typing import Any
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@ -29,21 +28,6 @@ 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)
|
||||||
@ -55,13 +39,7 @@ 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)
|
|
||||||
|
|||||||
@ -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) {
|
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 () {
|
es.onopen = function () {
|
||||||
console.log('connected');
|
console.log('connected');
|
||||||
@ -36,27 +29,6 @@ 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 = "";
|
||||||
}
|
}
|
||||||
@ -65,19 +37,6 @@ var start = function (url) {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, false);
|
}, 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 uuid import UUID
|
||||||
|
|
||||||
from celery import shared_task
|
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
|
from deployment.models import Deployment, Type, Status
|
||||||
|
|
||||||
|
|
||||||
class DeploymentTask(AbortableTask):
|
@shared_task
|
||||||
def try_exec(self, stop: int):
|
def deploy(deployment_id: UUID):
|
||||||
if stop == 0:
|
deploy = Deployment.objects.get(id=deployment_id)
|
||||||
stop = 1
|
|
||||||
|
|
||||||
for i in range(1, stop):
|
deploy.status = Status.RUNNING.name
|
||||||
time.sleep(5 * stop - 2 * i)
|
deploy.save()
|
||||||
|
|
||||||
if random.randint(0, 10) == 10:
|
match deploy.type:
|
||||||
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:
|
|
||||||
case Type.SLIM.name:
|
case Type.SLIM.name:
|
||||||
self.run_slim()
|
time.sleep(10)
|
||||||
case Type.MEDIUM.name:
|
case Type.MEDIUM.name:
|
||||||
self.run_medium()
|
time.sleep(60)
|
||||||
case Type.LARGE.name:
|
case Type.LARGE.name:
|
||||||
self.run_large()
|
time.sleep(120)
|
||||||
|
|
||||||
self.deploy.task_id = None
|
deploy.status = (
|
||||||
self.deploy.save()
|
Status.FAILED.name if random.randint(0, 10) % 2 != 0 else Status.SUCCESS.name
|
||||||
|
)
|
||||||
|
deploy.save()
|
||||||
|
|
||||||
Event.send(self.deploy)
|
send_event("deployment", "message", {"id": deploy.id, "status": deploy.status})
|
||||||
|
print("event sent")
|
||||||
|
|
||||||
@shared_task(base=DeploymentTask, bind=True, ignore_result=True)
|
|
||||||
def deploy(self, deployment_id: UUID):
|
|
||||||
self.launch(deployment_id)
|
|
||||||
|
|||||||
@ -1,23 +1,17 @@
|
|||||||
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, abort
|
from deployment.views import index, create, details, deploy
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index, name="deployments"),
|
path("", index, name="deployment"),
|
||||||
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/<user_id>/",
|
"events/",
|
||||||
include(django_eventstream.urls),
|
include(django_eventstream.urls),
|
||||||
{"format-channels": ["deployment_{user_id}"]},
|
{"channels": ["deployment"]},
|
||||||
name="deployments-events",
|
name="deployment-events",
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/<user_id>/<deployment_id>/",
|
|
||||||
include(django_eventstream.urls),
|
|
||||||
{"format-channels": ["deployment_{user_id}_{deployment_id}"]},
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,39 +1,18 @@
|
|||||||
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"
|
||||||
@ -48,9 +27,8 @@ 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": f"events/{request.user.id}/",
|
"url": "events/",
|
||||||
"can_create": check_user_credits(request.user),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -59,85 +37,35 @@ 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"/deployments?page={page}")
|
return HttpResponseRedirect(f"/deployment?page={page}")
|
||||||
|
|
||||||
return HttpResponseRedirect("/deployments")
|
return HttpResponseRedirect("/deployment")
|
||||||
|
|
||||||
|
|
||||||
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("/deployments")
|
return HttpResponseRedirect("/deployment")
|
||||||
|
|
||||||
return render(
|
return render(request, "deployment/details.html", {"deployment": deployment})
|
||||||
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":
|
||||||
if not check_user_credits(request.user):
|
|
||||||
return HttpResponseForbidden(
|
|
||||||
"unable to launch deployment, contact administrator for support"
|
|
||||||
)
|
|
||||||
|
|
||||||
form = DeploymentForm(request.POST)
|
form = DeploymentForm(request.POST)
|
||||||
if not form.is_valid():
|
if form.is_valid():
|
||||||
return HttpResponseBadRequest(
|
|
||||||
f"deployment creation inputs are invalid: {form.errors}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Deployment.objects.create(
|
Deployment.objects.create(
|
||||||
user=request.user, id=uuid4(), **form.cleaned_data
|
user=request.user, id=uuid4(), **form.cleaned_data
|
||||||
@ -145,4 +73,4 @@ def create(request):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return HttpResponseServerError(e)
|
return HttpResponseServerError(e)
|
||||||
|
|
||||||
return HttpResponseRedirect("/deployments")
|
return HttpResponseRedirect("/deployment")
|
||||||
|
|||||||
@ -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-mumui
|
container_name: redis
|
||||||
networks:
|
networks:
|
||||||
- mumui_network
|
- mumui_network
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
|
|
||||||
pushpin:
|
pushpin:
|
||||||
image: pushpin:mumui
|
image: pushpin:local
|
||||||
container_name: pushpin-mumui
|
container_name: pushpin
|
||||||
networks:
|
networks:
|
||||||
- mumui_network
|
- mumui_network
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -18,9 +18,11 @@ services:
|
|||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
container_name: postgres-mumui
|
container_name: postgres
|
||||||
env_file:
|
environment:
|
||||||
- .env
|
POSTGRES_DB: mumui
|
||||||
|
POSTGRES_USER: test
|
||||||
|
POSTGRES_PASSWORD: test
|
||||||
networks:
|
networks:
|
||||||
- mumui_network
|
- mumui_network
|
||||||
volumes:
|
volumes:
|
||||||
@ -29,8 +31,6 @@ 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:mumui
|
image: nginx:local
|
||||||
container_name: nginx-mumui
|
container_name: nginx
|
||||||
networks:
|
networks:
|
||||||
- mumui_network
|
- mumui_network
|
||||||
volumes:
|
volumes:
|
||||||
@ -50,6 +50,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- mumui
|
- mumui
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
mumui_network:
|
mumui_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@ -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", backend="redis://redis:6379/0")
|
app = Celery("mumui", broker="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()
|
||||||
|
|||||||
@ -9,7 +9,6 @@ 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
|
||||||
|
|
||||||
@ -21,18 +20,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|||||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = os.environ["SECRET_KEY"]
|
SECRET_KEY = "django-insecure-_c56%%c8%g%@5(3&thxi7ku2a&wst8lik*8@l0=#)ar)s86g36"
|
||||||
|
|
||||||
IS_PROD = os.getenv("ENV") == "prod"
|
|
||||||
|
|
||||||
HOSTS = ["*"]
|
|
||||||
if IS_PROD:
|
|
||||||
HOSTS = os.environ["HOSTS"].split(",")
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# 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
|
# Application definition
|
||||||
@ -87,8 +80,8 @@ DATABASES = {
|
|||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
"NAME": "mumui",
|
"NAME": "mumui",
|
||||||
"USER": os.getenv("POSTGRES_USER", "test"),
|
"USER": "test",
|
||||||
"PASSWORD": os.getenv("POSTGRES_PASSWORD", "test"),
|
"PASSWORD": "test",
|
||||||
"HOST": "postgres",
|
"HOST": "postgres",
|
||||||
"PORT": "5432",
|
"PORT": "5432",
|
||||||
}
|
}
|
||||||
@ -140,6 +133,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.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 = [
|
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("deployments/", include("deployment.urls")),
|
path("deployment/", include("deployment.urls")),
|
||||||
path("", TemplateView.as_view(template_name="home.html"), name="home"),
|
path("", TemplateView.as_view(template_name="home.html"), name="home"),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
FROM fanout/pushpin:latest
|
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 makemigrations
|
||||||
python manage.py migrate
|
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
|
supervisord -c /app/supervisord.conf
|
||||||
@ -4,17 +4,27 @@
|
|||||||
|
|
||||||
{% block title %} Deployments board {% endblock %}
|
{% 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 %}
|
{% 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-10 col-sm-12">
|
<div class="col-lg-8 col-md-8 col-sm-6">
|
||||||
{% 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-1"></th>
|
<th class="col-2"></th>
|
||||||
<th class="col-2"></th>
|
<th class="col-2"></th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for deployment in page_obj %}
|
{% for deployment in page_obj %}
|
||||||
@ -23,26 +33,22 @@
|
|||||||
<th name="type">{{ deployment.type }}</th>
|
<th name="type">{{ deployment.type }}</th>
|
||||||
<th name="status">{{ deployment.status }}</th>
|
<th name="status">{{ deployment.status }}</th>
|
||||||
<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>
|
</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 btn-sm" type="submit">Deploy</button>
|
<button class="btn btn-success" 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 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 class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
||||||
<span role="status">Deploying...</span>
|
<span role="status">Deploy</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 %}
|
||||||
@ -52,12 +58,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
{% include 'pagination.html' %}
|
{% 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
|
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 %}
|
||||||
@ -67,16 +70,10 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal %}
|
{% block modal %}
|
||||||
{% if can_create %}
|
|
||||||
{% include 'deployment/create_modal.html' %}
|
{% include 'deployment/create_modal.html' %}
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% 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 src="{% static 'deployment/js/event_source.js' %}" />
|
||||||
</script>
|
</script>
|
||||||
<script>start('{{ url|safe }}');</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %} Deployment details: {{ deployment.name }} {% endblock %}
|
{% block title %} Deployment details: {{ deployment.name }} {% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -21,48 +19,21 @@
|
|||||||
<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">Updated at</label>
|
<label for="updated-at">{{ deployment.updated_at }}</label>
|
||||||
</div>
|
</div>
|
||||||
{% if deployment.status == "RUNNING" %}
|
<form action="" method="post">
|
||||||
<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 %}
|
{% csrf_token %}
|
||||||
</form>
|
<button type="button" onclick="goBack()" class="btn btn-secondary">Back</button>
|
||||||
{% else %}
|
{% if deployment.status != "RUNNING" %}
|
||||||
<form id="abort-deployment" action="{% url 'deployment-abort' deployment.id %}" method="post">
|
<button type="submit" class="btn btn-danger">Delete</button>
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h4 style="text-align: center;">Please log in !</h4>
|
<h4 style="text-align: center;">Please log in !</h4>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -72,24 +43,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
{% if deployment.status == "RUNNING" or deployment.status == "PENDING" %}
|
<script>
|
||||||
<script src="{% static 'django_eventstream/json2.js' %}"></script>
|
function goBack() {
|
||||||
<script src="{% static 'django_eventstream/eventsource.min.js' %}"></script>
|
window.history.back();
|
||||||
<script src="{% static 'django_eventstream/reconnecting-eventsource.js' %}"></script>
|
}
|
||||||
<script src="{% static 'deployment/js/event_source_details.js' %}" /></script>
|
</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;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% 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">
|
<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">
|
||||||
@ -15,7 +12,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 'deployments' %}" class="nav-link">Deployments</a></li>
|
<a href="{% url 'deployment' %}" 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>
|
||||||
@ -30,3 +27,34 @@
|
|||||||
</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> -->
|
||||||
@ -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="{% url 'deployments' %}">Deployments</a>
|
<a href="/deployment/">Deployments</a>
|
||||||
and enjoy the life !
|
and enjoy the life !
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user