feat: notify user on instance creation #14
|
@ -4,7 +4,7 @@ pipeline:
|
||||||
environment:
|
environment:
|
||||||
- DATABSE_URL=postgres://postgres:password@database:5432/postgres
|
- DATABSE_URL=postgres://postgres:password@database:5432/postgres
|
||||||
- EMAIL_URL=smtp://admin:password@smtp:10025
|
- EMAIL_URL=smtp://admin:password@smtp:10025
|
||||||
- HOSTEA_INFRA_HOSTEA_REPO_REMOTE=ssh://git@gitea:22/hostea/fleet.git
|
- HOSTEA_INFRA_HOSTEA_REPO_REMOTE=ssh://git@gitea:22/hostea/
|
||||||
- HOSTEA_META_GITEA_INSTANCE=http://gitea:3000
|
- HOSTEA_META_GITEA_INSTANCE=http://gitea:3000
|
||||||
commands:
|
commands:
|
||||||
- export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"
|
- export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"
|
||||||
|
|
|
@ -2,20 +2,6 @@
|
||||||
<div class="footer__container">
|
<div class="footer__container">
|
||||||
<div class="footer__column">
|
<div class="footer__column">
|
||||||
<span class="license__conatiner">
|
<span class="license__conatiner">
|
||||||
<a class="license__link" rel="noreferrer" href="/docs" target="_blank"
|
|
||||||
>Docs</a
|
|
||||||
>
|
|
||||||
<span class="footer__column-divider--mobile-visible">|</span>
|
|
||||||
<a
|
|
||||||
class="license__link"
|
|
||||||
rel="noreferrer"
|
|
||||||
href="https://www.eff.org/issues/do-not-track/amp/"
|
|
||||||
target="_blank"
|
|
||||||
>No AMP</a
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="footer__column">
|
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="footer__link"
|
class="footer__link"
|
||||||
|
@ -23,6 +9,21 @@
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
title="RSS"
|
title="RSS"
|
||||||
>Home</a>
|
>Home</a>
|
||||||
|
<span class="footer__column-divider--mobile-visible">|</span>
|
||||||
|
<a class="license__link" rel="noreferrer" href="https://hostea.org/about" target="_blank"
|
||||||
|
> About</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer__column">
|
||||||
|
<a
|
||||||
|
class="license__link"
|
||||||
|
rel="noreferrer"
|
||||||
|
href="https://www.eff.org/issues/do-not-track/amp/"
|
||||||
|
target="_blank"
|
||||||
|
>No AMP</a
|
||||||
|
>
|
||||||
|
|
||||||
<div class="footer__column-divider">|</div>
|
<div class="footer__column-divider">|</div>
|
||||||
<a href="mailto:{{ footer.admin_email }}" class="footer__link"
|
<a href="mailto:{{ footer.admin_email }}" class="footer__link"
|
||||||
>Contact Instance Maintainer</a
|
>Contact Instance Maintainer</a
|
||||||
|
@ -30,12 +31,12 @@
|
||||||
<div class="footer__column-divider">|</div>
|
<div class="footer__column-divider">|</div>
|
||||||
<a
|
<a
|
||||||
class="footer__link"
|
class="footer__link"
|
||||||
href="{{ footer.source_code }}"
|
href="{{ footer.source_code.link }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
title="Source Code"
|
title="Source Code"
|
||||||
>
|
>
|
||||||
v{{ footer.version }}-{{ footer.git_hash }}
|
{{ footer.source_code.text }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,6 +23,7 @@ from django.http import HttpResponse
|
||||||
from django.views.decorators.csrf import csrf_protect
|
from django.views.decorators.csrf import csrf_protect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from dash.utils import footer_ctx
|
||||||
|
|
||||||
from .models import AccountConfirmChallenge
|
from .models import AccountConfirmChallenge
|
||||||
from .utils import send_verification_email, ConfirmAccess
|
from .utils import send_verification_email, ConfirmAccess
|
||||||
|
@ -35,6 +36,7 @@ def login_view(request):
|
||||||
def default_login_ctx():
|
def default_login_ctx():
|
||||||
return {
|
return {
|
||||||
"title": "Login",
|
"title": "Login",
|
||||||
|
"footer": footer_ctx(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
|
@ -102,6 +104,7 @@ def register_view(request):
|
||||||
"title": "Register",
|
"title": "Register",
|
||||||
"username": username,
|
"username": username,
|
||||||
"email": username,
|
"email": username,
|
||||||
|
"footer": footer_ctx(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
|
@ -213,6 +216,7 @@ def sudo(request):
|
||||||
def default_login_ctx():
|
def default_login_ctx():
|
||||||
return {
|
return {
|
||||||
"title": "Confirm Access",
|
"title": "Confirm Access",
|
||||||
|
"footer": footer_ctx(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
|
|
|
@ -26,7 +26,7 @@ from django.conf import settings
|
||||||
from payments import get_payment_model, RedirectNeeded, PaymentStatus
|
from payments import get_payment_model, RedirectNeeded, PaymentStatus
|
||||||
|
|
||||||
from accounts.tests import register_util, login_util
|
from accounts.tests import register_util, login_util
|
||||||
from dash.tests import create_configurations, create_instance_util
|
from dash.tests import create_configurations, create_instance_util, infra_custom_config
|
||||||
from dash.models import Instance
|
from dash.models import Instance
|
||||||
|
|
||||||
from .models import Payment
|
from .models import Payment
|
||||||
|
@ -43,55 +43,21 @@ class BillingTest(TestCase):
|
||||||
register_util(t=self, username=self.username)
|
register_util(t=self, username=self.username)
|
||||||
create_configurations(t=self)
|
create_configurations(t=self)
|
||||||
|
|
||||||
|
@override_settings(HOSTEA=infra_custom_config(test_name="test_payments"))
|
||||||
def test_payments(self):
|
def test_payments(self):
|
||||||
c = Client()
|
c = Client()
|
||||||
login_util(self, c, "accounts.home")
|
login_util(self, c, "accounts.home")
|
||||||
instance_name = "test_create_instance_renders"
|
instance_name = "test_payments"
|
||||||
create_instance_util(
|
create_instance_util(
|
||||||
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
|
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
instance = Instance.objects.get(name=instance_name)
|
instance = Instance.objects.get(name=instance_name)
|
||||||
|
|
||||||
payment_uri = reverse("billing.invoice.generate", args=(instance_name,))
|
|
||||||
|
|
||||||
# generate invoice
|
|
||||||
resp = c.get(payment_uri)
|
|
||||||
self.assertEqual(resp.status_code, 302)
|
|
||||||
invoice_uri = resp.headers["Location"]
|
|
||||||
self.assertEqual("invoice/payment/" in invoice_uri, True)
|
|
||||||
|
|
||||||
# try to generate duplicate invoice, but should get redirected to previous invoice
|
|
||||||
resp = c.get(payment_uri)
|
|
||||||
self.assertEqual(resp.status_code, 302)
|
|
||||||
self.assertEqual(invoice_uri == resp.headers["Location"], True)
|
|
||||||
|
|
||||||
# check if invoice details page is displaying the invoice
|
|
||||||
# if payment is yet to be made:
|
|
||||||
# template will show payment button
|
|
||||||
# else:
|
|
||||||
# template will show payment date
|
|
||||||
resp = c.get(invoice_uri)
|
|
||||||
self.assertEqual(str.encode(instance_name) in resp.content, True)
|
|
||||||
self.assertEqual(
|
|
||||||
str.encode(str(self.instance_config[0].rent)) in resp.content, True
|
|
||||||
)
|
|
||||||
self.assertEqual(str.encode("Paid on") in resp.content, False)
|
|
||||||
|
|
||||||
# check if the unpaid invoice is displayed in the pending invoice view
|
|
||||||
resp = c.get(reverse("billing.invoice.pending"))
|
|
||||||
self.assertEqual(str.encode(invoice_uri) in resp.content, True)
|
|
||||||
|
|
||||||
self.assertEqual(payment_fullfilled(instance=instance), False)
|
|
||||||
|
|
||||||
# simulate payment. There's probably a better way to do this
|
|
||||||
payment = get_payment_model().objects.get(paid_by=self.user)
|
|
||||||
payment.status = PaymentStatus.CONFIRMED
|
|
||||||
payment.save()
|
|
||||||
|
|
||||||
self.assertEqual(payment_fullfilled(instance=instance), True)
|
self.assertEqual(payment_fullfilled(instance=instance), True)
|
||||||
|
|
||||||
#
|
payment = get_payment_model().objects.get(paid_by=self.user)
|
||||||
|
invoice_uri = reverse("billing.invoice.details", args=(payment.public_ref,))
|
||||||
|
|
||||||
# check if paid invoice is listed in paid invoice list view
|
# check if paid invoice is listed in paid invoice list view
|
||||||
resp = c.get(reverse("billing.invoice.paid"))
|
resp = c.get(reverse("billing.invoice.paid"))
|
||||||
|
@ -111,6 +77,7 @@ class BillingTest(TestCase):
|
||||||
|
|
||||||
# try to generate an invoice for the second time on the same VM
|
# try to generate an invoice for the second time on the same VM
|
||||||
# shouldn't be possible since payment is already made for the duration
|
# shouldn't be possible since payment is already made for the duration
|
||||||
|
payment_uri = reverse("billing.invoice.generate", args=(instance.name,))
|
||||||
resp = c.get(payment_uri)
|
resp = c.get(payment_uri)
|
||||||
self.assertEqual(resp.status_code, 400)
|
self.assertEqual(resp.status_code, 400)
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ from payments import get_payment_model, RedirectNeeded, PaymentStatus
|
||||||
from dash.models import Instance
|
from dash.models import Instance
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from infrastructure.utils import create_vm_if_not_exists
|
from infrastructure.utils import create_vm_if_not_exists
|
||||||
|
from dash.utils import footer_ctx
|
||||||
|
|
||||||
|
|
||||||
def default_ctx(title: str, username: str):
|
def default_ctx(title: str, username: str):
|
||||||
|
@ -33,6 +34,7 @@ def default_ctx(title: str, username: str):
|
||||||
return {
|
return {
|
||||||
"title": title,
|
"title": title,
|
||||||
"username": username,
|
"username": username,
|
||||||
|
"footer": footer_ctx(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>Created On: {{ instance.created_at }}</p>
|
<p>Created On: {{ instance.created_at }}</p>
|
||||||
|
<p><a href="{{gitea_uri}}">Gitea Instance</a>|<a href="{{woodpecker}}">Woodpecker CI</a></p>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
action="{% url 'dash.instances.delete' name=instance.name %}"
|
action="{% url 'dash.instances.delete' name=instance.name %}"
|
||||||
|
|
|
@ -12,12 +12,20 @@
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
from time import sleep
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.test import TestCase, Client, override_settings
|
from django.test import TestCase, Client, override_settings
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
from payments import get_payment_model, RedirectNeeded, PaymentStatus
|
||||||
|
|
||||||
from accounts.tests import login_util, register_util
|
from accounts.tests import login_util, register_util
|
||||||
|
|
||||||
|
@ -44,6 +52,28 @@ def create_configurations(t: TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def infra_custom_config(test_name: str):
|
||||||
|
def create_fleet_repo(test_name: str):
|
||||||
|
subprocess.run(
|
||||||
|
["./integration/ci.sh", "new_fleet_repo", test_name],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
sleep(10)
|
||||||
|
|
||||||
|
create_fleet_repo(test_name=test_name)
|
||||||
|
|
||||||
|
c = settings.HOSTEA
|
||||||
|
path = Path(f"/tmp/hostea/dashboard/{test_name}/repo")
|
||||||
|
if path.exists():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
c["INFRA"]["HOSTEA_REPO"]["PATH"] = str(path)
|
||||||
|
remote_base = os.environ.get("HOSTEA_INFRA_HOSTEA_REPO_REMOTE")
|
||||||
|
c["INFRA"]["HOSTEA_REPO"]["REMOTE"] = f"{remote_base}{test_name}.git"
|
||||||
|
print(c["INFRA"]["HOSTEA_REPO"]["REMOTE"])
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
def create_instance_util(
|
def create_instance_util(
|
||||||
t: TestCase, c: Client, instance_name: str, config: InstanceConfiguration
|
t: TestCase, c: Client, instance_name: str, config: InstanceConfiguration
|
||||||
):
|
):
|
||||||
|
@ -55,6 +85,22 @@ def create_instance_util(
|
||||||
resp.headers["location"],
|
resp.headers["location"],
|
||||||
reverse("billing.invoice.generate", args=(instance_name,)),
|
reverse("billing.invoice.generate", args=(instance_name,)),
|
||||||
)
|
)
|
||||||
|
# generate invoice
|
||||||
|
payment_uri = reverse("billing.invoice.generate", args=(instance_name,))
|
||||||
|
resp = c.get(payment_uri)
|
||||||
|
t.assertEqual(resp.status_code, 302)
|
||||||
|
invoice_uri = resp.headers["Location"]
|
||||||
|
t.assertEqual("invoice/payment/" in invoice_uri, True)
|
||||||
|
|
||||||
|
# simulate payment. There's probably a better way to do this
|
||||||
|
payment = get_payment_model().objects.get(
|
||||||
|
paid_by=t.user, instance_name=instance_name
|
||||||
|
)
|
||||||
|
payment.status = PaymentStatus.CONFIRMED
|
||||||
|
payment.save()
|
||||||
|
|
||||||
|
resp = c.get(reverse("infra.create", args=(instance_name,)))
|
||||||
|
t.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class DashHome(TestCase):
|
class DashHome(TestCase):
|
||||||
|
@ -151,6 +197,9 @@ class CreateInstance(TestCase):
|
||||||
register_util(t=self, username="createinstance_user")
|
register_util(t=self, username="createinstance_user")
|
||||||
create_configurations(t=self)
|
create_configurations(t=self)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
HOSTEA=infra_custom_config(test_name="test_create_instance_renders")
|
||||||
|
)
|
||||||
def test_create_instance_renders(self):
|
def test_create_instance_renders(self):
|
||||||
c = Client()
|
c = Client()
|
||||||
login_util(self, c, "accounts.home")
|
login_util(self, c, "accounts.home")
|
||||||
|
@ -236,12 +285,16 @@ class CreateInstance(TestCase):
|
||||||
|
|
||||||
resp = c.post(delete_uri)
|
resp = c.post(delete_uri)
|
||||||
self.assertEqual(resp.status_code, 302)
|
self.assertEqual(resp.status_code, 302)
|
||||||
self.assertEqual(resp.headers["location"], reverse("dash.home"))
|
self.assertEqual(
|
||||||
|
resp.headers["location"], reverse("infra.rm", args=(instance.name,))
|
||||||
|
)
|
||||||
|
resp = c.get(resp.headers["location"])
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
self.assertEqual(resp.headers["location"], reverse("dash.instances.list"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Instance.objects.filter(
|
Instance.objects.filter(
|
||||||
name=instance.name,
|
name=instance_name,
|
||||||
owned_by=self.user,
|
owned_by=self.user,
|
||||||
configuration_id=self.instance_config[0],
|
|
||||||
).exists(),
|
).exists(),
|
||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,7 +14,9 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
|
|
||||||
|
from git import Repo
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from .models import Instance, InstanceConfiguration
|
from .models import Instance, InstanceConfiguration
|
||||||
|
|
||||||
|
@ -55,3 +57,30 @@ def create_instance(vm_name: str, configuration_name: str, user: User) -> Instan
|
||||||
instance = Instance(name=vm_name, configuration_id=configuration, owned_by=user)
|
instance = Instance(name=vm_name, configuration_id=configuration, owned_by=user)
|
||||||
instance.save()
|
instance.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
source_code = None
|
||||||
|
|
||||||
|
|
||||||
|
def footer_ctx():
|
||||||
|
global source_code
|
||||||
|
if source_code is None:
|
||||||
|
if "SOURCE_CODE" in settings.HOSTEA:
|
||||||
|
source_code = {
|
||||||
|
"text": "Source Code",
|
||||||
|
"link": settings.HOSTEA["SOURCE_CODE"],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
link = "https://gitea.hostea.org/Hostea/dashboard"
|
||||||
|
source_code = {"text": "Source Code", "link": link}
|
||||||
|
try:
|
||||||
|
r = Repo(".")
|
||||||
|
commit = r.head.commit.hexsha
|
||||||
|
source_code["text"] = f"v-{commit.hexsha[0:8]}"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"source_code": source_code,
|
||||||
|
"admin_email": settings.HOSTEA["INSTANCE_MAINTAINER_CONTACT"],
|
||||||
|
}
|
||||||
|
|
|
@ -22,9 +22,15 @@ from django.views.decorators.csrf import csrf_protect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from accounts.decorators import confirm_access
|
from accounts.decorators import confirm_access
|
||||||
|
from infrastructure.utils import Infra
|
||||||
|
|
||||||
from .models import Instance, InstanceConfiguration
|
from .models import Instance, InstanceConfiguration
|
||||||
from .utils import create_instance as create_instance_util, VmErrors, VmException
|
from .utils import (
|
||||||
|
create_instance as create_instance_util,
|
||||||
|
VmErrors,
|
||||||
|
VmException,
|
||||||
|
footer_ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def default_ctx(title: str, username: str):
|
def default_ctx(title: str, username: str):
|
||||||
|
@ -35,6 +41,7 @@ def default_ctx(title: str, username: str):
|
||||||
"title": title,
|
"title": title,
|
||||||
"username": username,
|
"username": username,
|
||||||
"open_instances": "open",
|
"open_instances": "open",
|
||||||
|
"footer": footer_ctx(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,7 +120,12 @@ def view_instance(request, name: str):
|
||||||
instance = get_object_or_404(Instance, owned_by=user, name=name)
|
instance = get_object_or_404(Instance, owned_by=user, name=name)
|
||||||
ctx = default_ctx(title=PAGE_TITLE, username=user.username)
|
ctx = default_ctx(title=PAGE_TITLE, username=user.username)
|
||||||
instance.configuration = instance.configuration_id
|
instance.configuration = instance.configuration_id
|
||||||
|
gitea_uri = Infra.get_gitea_uri(instance=instance)
|
||||||
|
woodpecker = Infra.get_woodpecker_uri(instance=instance)
|
||||||
|
|
||||||
ctx["instance"] = instance
|
ctx["instance"] = instance
|
||||||
|
ctx["woodpecker"] = woodpecker
|
||||||
|
ctx["gitea_uri"] = gitea_uri
|
||||||
return render(request, "dash/instances/view/index.html", context=ctx)
|
return render(request, "dash/instances/view/index.html", context=ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@ -130,5 +142,4 @@ def delete_instance(request, name):
|
||||||
ctx["instance"] = instance
|
ctx["instance"] = instance
|
||||||
return render(request, "dash/instances/delete/index.html", context=ctx)
|
return render(request, "dash/instances/delete/index.html", context=ctx)
|
||||||
|
|
||||||
instance.delete()
|
return redirect(reverse("infra.rm", args=(instance.name,)))
|
||||||
return redirect(reverse("dash.home"))
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ PAYMENT_VARIANTS = {
|
||||||
### Dashbaord specific configuration options
|
### Dashbaord specific configuration options
|
||||||
|
|
||||||
HOSTEA = {
|
HOSTEA = {
|
||||||
|
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
|
||||||
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
||||||
"ACCOUNTS": {
|
"ACCOUNTS": {
|
||||||
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
||||||
|
|
|
@ -53,6 +53,7 @@ PAYMENT_VARIANTS = {
|
||||||
### Dashbaord specific configuration options
|
### Dashbaord specific configuration options
|
||||||
|
|
||||||
HOSTEA = {
|
HOSTEA = {
|
||||||
|
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
|
||||||
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
||||||
"ACCOUNTS": {
|
"ACCOUNTS": {
|
||||||
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
||||||
|
|
|
@ -168,6 +168,7 @@ PAYMENT_VARIANTS = {
|
||||||
### Dashbaord specific configuration options
|
### Dashbaord specific configuration options
|
||||||
|
|
||||||
HOSTEA = {
|
HOSTEA = {
|
||||||
|
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
|
||||||
"RESTRICT_NEW_INTEGRATION_INSTALLATION": True,
|
"RESTRICT_NEW_INTEGRATION_INSTALLATION": True,
|
||||||
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
||||||
"ACCOUNTS": {
|
"ACCOUNTS": {
|
||||||
|
|
|
@ -119,7 +119,7 @@ class Command(BaseCommand):
|
||||||
vm_name = options[self.vm_name_key]
|
vm_name = options[self.vm_name_key]
|
||||||
if Instance.objects.filter(name=vm_name).exists():
|
if Instance.objects.filter(name=vm_name).exists():
|
||||||
instance = Instance.objects.get(name=vm_name)
|
instance = Instance.objects.get(name=vm_name)
|
||||||
delete_vm(instance=instance, owner=instance.owned_by.username)
|
delete_vm(instance=instance)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
for i in [self.action_key, self.vm_name_key]:
|
for i in [self.action_key, self.vm_name_key]:
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Generated by Django 4.0.3 on 2022-06-29 18:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("dash", "0006_auto_20220619_0800"),
|
||||||
|
("infrastructure", "0005_remove_instancecreated_gitea_password"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Job",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("job_type", models.CharField(max_length=10, verbose_name="Job Type")),
|
||||||
|
(
|
||||||
|
"instance",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="dash.instance"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -12,11 +12,30 @@
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
from django.db import models
|
from enum import Enum, unique
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
from dash.models import Instance
|
from dash.models import Instance
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class JobType(Enum):
|
||||||
|
PING = "ping"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class InstanceCreated(models.Model):
|
class InstanceCreated(models.Model):
|
||||||
instance = models.ForeignKey(Instance, on_delete=models.PROTECT)
|
instance = models.ForeignKey(Instance, on_delete=models.PROTECT)
|
||||||
created = models.BooleanField(default=False)
|
created = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Job(models.Model):
|
||||||
|
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
job_type = models.CharField(
|
||||||
|
"Job Type",
|
||||||
|
max_length=10,
|
||||||
|
null=False,
|
||||||
|
)
|
||||||
|
|
|
@ -14,12 +14,11 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from urllib.parse import urlparse, urlunparse
|
from urllib.parse import urlunparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
from django.test import TestCase, Client, override_settings
|
from django.test import TestCase, Client, override_settings
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
@ -28,20 +27,11 @@ from git import Repo
|
||||||
|
|
||||||
from dash.models import Instance, InstanceConfiguration
|
from dash.models import Instance, InstanceConfiguration
|
||||||
from accounts.tests import register_util, login_util
|
from accounts.tests import register_util, login_util
|
||||||
from dash.tests import create_configurations, create_instance_util
|
from dash.tests import create_configurations, create_instance_util, infra_custom_config
|
||||||
from infrastructure.management.commands.vm import translate_sizes
|
from infrastructure.management.commands.vm import translate_sizes
|
||||||
|
|
||||||
from .utils import Infra
|
from .utils import Infra, Worker
|
||||||
from .models import InstanceCreated
|
from .models import InstanceCreated, Job, JobType
|
||||||
|
|
||||||
|
|
||||||
def custom_config(test_name: str):
|
|
||||||
c = settings.HOSTEA
|
|
||||||
path = Path(f"/tmp/hostea/dashboard/{test_name}/repo")
|
|
||||||
if path.exists():
|
|
||||||
shutil.rmtree(path)
|
|
||||||
c["INFRA"]["HOSTEA_REPO"]["PATH"] = str(path)
|
|
||||||
return c
|
|
||||||
|
|
||||||
|
|
||||||
class InfraUtilTest(TestCase):
|
class InfraUtilTest(TestCase):
|
||||||
|
@ -54,7 +44,7 @@ class InfraUtilTest(TestCase):
|
||||||
register_util(t=self, username=self.username)
|
register_util(t=self, username=self.username)
|
||||||
create_configurations(t=self)
|
create_configurations(t=self)
|
||||||
|
|
||||||
@override_settings(HOSTEA=custom_config(test_name="test_path_util"))
|
@override_settings(HOSTEA=infra_custom_config(test_name="test_path_util"))
|
||||||
def test_path_utils(self):
|
def test_path_utils(self):
|
||||||
infra = Infra()
|
infra = Infra()
|
||||||
subdomain = "foo"
|
subdomain = "foo"
|
||||||
|
@ -85,7 +75,7 @@ class InfraUtilTest(TestCase):
|
||||||
infra._hostscript_path(subdomain=subdomain),
|
infra._hostscript_path(subdomain=subdomain),
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(HOSTEA=custom_config(test_name="test_add_vm"))
|
@override_settings(HOSTEA=infra_custom_config(test_name="test_add_vm"))
|
||||||
def test_add_vm(self):
|
def test_add_vm(self):
|
||||||
infra = Infra()
|
infra = Infra()
|
||||||
c = Client()
|
c = Client()
|
||||||
|
@ -105,9 +95,12 @@ class InfraUtilTest(TestCase):
|
||||||
after_add = infra.repo.head.commit.hexsha
|
after_add = infra.repo.head.commit.hexsha
|
||||||
self.assertEqual(before_add is not after_add, True)
|
self.assertEqual(before_add is not after_add, True)
|
||||||
|
|
||||||
c = custom_config(test_name="test_add_vm--get-head")
|
# c = infra_custom_config(test_name="test_add_vm--get-head")
|
||||||
path = c["INFRA"]["HOSTEA_REPO"]["PATH"]
|
path = Path("/tmp/hostea/dashboard/check-test_add_vm")
|
||||||
|
if path.exists():
|
||||||
|
shutil.rmtree(path)
|
||||||
repo = Repo.clone_from(conf["REMOTE"], path, env=infra.env)
|
repo = Repo.clone_from(conf["REMOTE"], path, env=infra.env)
|
||||||
|
repo.git.pull(env=infra.env)
|
||||||
self.assertEqual(repo.head.commit.hexsha == after_add, True)
|
self.assertEqual(repo.head.commit.hexsha == after_add, True)
|
||||||
|
|
||||||
before_rm = infra.repo.head.commit.hexsha
|
before_rm = infra.repo.head.commit.hexsha
|
||||||
|
@ -115,10 +108,10 @@ class InfraUtilTest(TestCase):
|
||||||
after_rm = infra.repo.head.commit.hexsha
|
after_rm = infra.repo.head.commit.hexsha
|
||||||
self.assertEqual(before_add is not after_add, True)
|
self.assertEqual(before_add is not after_add, True)
|
||||||
|
|
||||||
repo.git.pull()
|
repo.git.pull(env=infra.env)
|
||||||
self.assertEqual(repo.head.commit.hexsha == after_rm, True)
|
self.assertEqual(repo.head.commit.hexsha == after_rm, True)
|
||||||
|
|
||||||
@override_settings(HOSTEA=custom_config(test_name="test_cmd"))
|
@override_settings(HOSTEA=infra_custom_config(test_name="test_cmd"))
|
||||||
def test_cmd(self):
|
def test_cmd(self):
|
||||||
subdomain = "cmd_vm"
|
subdomain = "cmd_vm"
|
||||||
infra = Infra()
|
infra = Infra()
|
||||||
|
@ -187,3 +180,26 @@ class InfraUtilTest(TestCase):
|
||||||
|
|
||||||
# run delete VM command to crudely check idempotency
|
# run delete VM command to crudely check idempotency
|
||||||
call_command("vm", "delete", subdomain)
|
call_command("vm", "delete", subdomain)
|
||||||
|
|
||||||
|
def test_worker(self):
|
||||||
|
subdomain = "gitea" # yes, gitea.hostea.org exists. will use it till I
|
||||||
|
# figure out how to use requests_mock within django
|
||||||
|
c = Client()
|
||||||
|
login_util(self, c, "accounts.home")
|
||||||
|
create_instance_util(
|
||||||
|
t=self, c=c, instance_name=subdomain, config=self.instance_config[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
instance = Instance.objects.get(name=subdomain)
|
||||||
|
job = Job.objects.create(instance=instance, job_type=JobType.PING)
|
||||||
|
gitea_uri = Infra.get_gitea_uri(instance=instance)
|
||||||
|
print(f"mocking {gitea_uri}")
|
||||||
|
|
||||||
|
w = Worker(job=job)
|
||||||
|
w.start()
|
||||||
|
time.sleep(15)
|
||||||
|
self.assertEqual(w.is_alive(), False)
|
||||||
|
w.join()
|
||||||
|
self.assertEqual(
|
||||||
|
Job.objects.filter(instance=instance, job_type=JobType.PING).exists(), True
|
||||||
|
)
|
||||||
|
|
|
@ -15,17 +15,60 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import yaml
|
import yaml
|
||||||
|
import requests
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from threading import Thread, Event
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
from django.core.mail import send_mail
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from git import Repo, Commit
|
from git import Repo, Commit
|
||||||
from git.exc import InvalidGitRepositoryError
|
from git.exc import InvalidGitRepositoryError
|
||||||
|
|
||||||
from dash.models import Instance
|
from dash.models import Instance
|
||||||
|
|
||||||
from .models import InstanceCreated
|
from infrastructure.models import InstanceCreated, JobType, Job
|
||||||
|
|
||||||
|
|
||||||
|
class Worker(Thread):
|
||||||
|
def __init__(self, job: Job):
|
||||||
|
self.job = job
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
gitea_uri = Infra.get_gitea_uri(instance=self.job.instance)
|
||||||
|
woodpecker = Infra.get_woodpecker_uri(instance=self.job.instance)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(f"[ping] Trying to reach {gitea_uri}")
|
||||||
|
resp = requests.get(gitea_uri)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
sleep(10)
|
||||||
|
|
||||||
|
job = self.job
|
||||||
|
self.job = None
|
||||||
|
email = job.instance.owned_by.email
|
||||||
|
send_mail(
|
||||||
|
subject="[Hostea] Your Hostea instance is now online!",
|
||||||
|
message=f"""
|
||||||
|
Hello,
|
||||||
|
|
||||||
|
The deployment job has run to completion and your Hostea instance is now online!
|
||||||
|
Credentials to admin account was sent in an earlier email, please contact
|
||||||
|
support if didn't receive it.
|
||||||
|
|
||||||
|
Gitea: {gitea_uri}
|
||||||
|
Woodpecker CI: {woodpecker}
|
||||||
|
""",
|
||||||
|
from_email="No reply Hostea<no-reply@exampl.org>", # TODO read from settings.py
|
||||||
|
recipient_list=[email],
|
||||||
|
)
|
||||||
|
job.delete()
|
||||||
|
|
||||||
|
|
||||||
def create_vm_if_not_exists(instance: Instance) -> (str, Commit):
|
def create_vm_if_not_exists(instance: Instance) -> (str, Commit):
|
||||||
|
@ -35,18 +78,20 @@ def create_vm_if_not_exists(instance: Instance) -> (str, Commit):
|
||||||
infra = Infra()
|
infra = Infra()
|
||||||
if not InstanceCreated.objects.filter(instance=instance).exists():
|
if not InstanceCreated.objects.filter(instance=instance).exists():
|
||||||
(gitea_password, commit) = infra.add_vm(instance=instance)
|
(gitea_password, commit) = infra.add_vm(instance=instance)
|
||||||
instance = InstanceCreated.objects.create(instance=instance, created=True)
|
InstanceCreated.objects.create(instance=instance, created=True)
|
||||||
instance.save()
|
job = Job.objects.create(instance=instance, job_type=str(JobType.PING))
|
||||||
|
Worker(job=job).start()
|
||||||
return (gitea_password, commit)
|
return (gitea_password, commit)
|
||||||
else:
|
else:
|
||||||
if str.strip(infra.get_flavor(instance=instance)) != str.strip(
|
if str.strip(infra.get_flavor(instance=instance)) != str.strip(
|
||||||
infra.translate_size(instance=instance)
|
infra.translate_size(instance=instance)
|
||||||
):
|
):
|
||||||
|
# Worker.init_global()
|
||||||
return infra.add_vm(instance=instance)
|
return infra.add_vm(instance=instance)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def delete_vm(instance: Instance, owner: str):
|
def delete_vm(instance: Instance):
|
||||||
infra = Infra()
|
infra = Infra()
|
||||||
infra.remove_vm(instance=instance)
|
infra.remove_vm(instance=instance)
|
||||||
if InstanceCreated.objects.filter(instance=instance).exists():
|
if InstanceCreated.objects.filter(instance=instance).exists():
|
||||||
|
@ -97,10 +142,10 @@ class Infra:
|
||||||
"""
|
"""
|
||||||
Get Woodpecker hostname of an instance
|
Get Woodpecker hostname of an instance
|
||||||
"""
|
"""
|
||||||
return (f"{instance.name}-ci",)
|
return f"{instance.name}-ci"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_woodpecker_hostname(cls, instance: Instance) -> str:
|
def get_woodpecker_uri(cls, instance: Instance) -> str:
|
||||||
"""
|
"""
|
||||||
Get an instance's Gitea URI
|
Get an instance's Gitea URI
|
||||||
"""
|
"""
|
||||||
|
@ -179,7 +224,9 @@ class Infra:
|
||||||
)
|
)
|
||||||
|
|
||||||
def _pull(self):
|
def _pull(self):
|
||||||
self.repo.git.pull(env=self.env, rebase="true")
|
self.repo.git.fetch(env=self.env)
|
||||||
|
|
||||||
|
# TODO: switch to using Git cmd
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def translate_size(instance: Instance) -> str:
|
def translate_size(instance: Instance) -> str:
|
||||||
|
|
|
@ -26,7 +26,7 @@ from accounts.decorators import confirm_access
|
||||||
from dash.models import Instance
|
from dash.models import Instance
|
||||||
from billing.utils import payment_fullfilled
|
from billing.utils import payment_fullfilled
|
||||||
|
|
||||||
from .utils import create_vm_if_not_exists, Infra
|
from .utils import create_vm_if_not_exists, Infra, delete_vm
|
||||||
|
|
||||||
|
|
||||||
def default_ctx(title: str, username: str):
|
def default_ctx(title: str, username: str):
|
||||||
|
@ -84,7 +84,5 @@ def delete_instance(request, instance_name: str):
|
||||||
Dashboard homepage view
|
Dashboard homepage view
|
||||||
"""
|
"""
|
||||||
instance = get_object_or_404(Instance, name=instance_name, owned_by=request.user)
|
instance = get_object_or_404(Instance, name=instance_name, owned_by=request.user)
|
||||||
infra = Infra()
|
delete_vm(instance=instance)
|
||||||
infra.remove_vm(instance=instance)
|
return redirect(reverse("dash.instances.list"))
|
||||||
# TODO: push isn't implemented yet
|
|
||||||
return HttpResponse()
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ init() {
|
||||||
else
|
else
|
||||||
docker_compose_down || true
|
docker_compose_down || true
|
||||||
docker_compose_up
|
docker_compose_up
|
||||||
|
sed -i /localhost.*/d ~/.ssh/known_hosts
|
||||||
setup_env
|
setup_env
|
||||||
sleep 5
|
sleep 5
|
||||||
# wait_for_env
|
# wait_for_env
|
||||||
|
@ -29,4 +30,8 @@ teardown() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
$1
|
new_fleet_repo() {
|
||||||
|
new_fleet_repo_init $2
|
||||||
|
}
|
||||||
|
|
||||||
|
$1 $@
|
||||||
|
|
|
@ -120,31 +120,20 @@ support_repo_init() {
|
||||||
$GITEA_HOSTEA_SUPPORT_REPO
|
$GITEA_HOSTEA_SUPPORT_REPO
|
||||||
}
|
}
|
||||||
|
|
||||||
# register user "Hostea" on Gitea and create support repository
|
new_fleet_repo_init() {
|
||||||
fleet_repo_init() {
|
|
||||||
python -m integration \
|
|
||||||
gitea register \
|
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
|
||||||
$GITEA_HOSTEA_EMAIL \
|
|
||||||
$GITEA_URL || true
|
|
||||||
python -m integration \
|
|
||||||
gitea login \
|
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
|
||||||
$GITEA_HOSTEA_EMAIL \
|
|
||||||
$GITEA_URL
|
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea create_repo \
|
gitea create_repo \
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
||||||
$GITEA_HOSTEA_EMAIL \
|
$GITEA_HOSTEA_EMAIL \
|
||||||
$GITEA_URL \
|
$GITEA_URL \
|
||||||
$GITEA_HOSTEA_FLEET_REPO
|
$1
|
||||||
|
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea add_deploy_key \
|
gitea add_deploy_key \
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
||||||
$GITEA_HOSTEA_EMAIL \
|
$GITEA_HOSTEA_EMAIL \
|
||||||
$GITEA_URL \
|
$GITEA_URL \
|
||||||
$GITEA_HOSTEA_FLEET_REPO \
|
$1 \
|
||||||
$GITEA_HOSTEA_FLEET_DEPLOY_KEY
|
$GITEA_HOSTEA_FLEET_DEPLOY_KEY
|
||||||
|
|
||||||
tmp_dir=$(mktemp -d)
|
tmp_dir=$(mktemp -d)
|
||||||
|
@ -159,7 +148,8 @@ fleet_repo_init() {
|
||||||
git init
|
git init
|
||||||
git add README
|
git add README
|
||||||
git commit -m "init"
|
git commit -m "init"
|
||||||
git remote add origin $GITEA_HOSTEA_FLEET_REPO_REMOTE
|
REMOTE="$GITEA_SSH_URL/$GITEA_HOSTEA_USERNAME/$1.git"
|
||||||
|
git remote add origin $REMOTE
|
||||||
GIT_SSH_COMMAND="/usr/bin/ssh -oStrictHostKeyChecking=no -i $GITEA_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE" \
|
GIT_SSH_COMMAND="/usr/bin/ssh -oStrictHostKeyChecking=no -i $GITEA_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE" \
|
||||||
git push --set-upstream origin master
|
git push --set-upstream origin master
|
||||||
popd
|
popd
|
||||||
|
@ -167,6 +157,24 @@ fleet_repo_init() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# register user "Hostea" on Gitea and create support repository
|
||||||
|
fleet_repo_init() {
|
||||||
|
python -m integration \
|
||||||
|
gitea register \
|
||||||
|
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
||||||
|
$GITEA_HOSTEA_EMAIL \
|
||||||
|
$GITEA_URL || true
|
||||||
|
python -m integration \
|
||||||
|
gitea login \
|
||||||
|
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
||||||
|
$GITEA_HOSTEA_EMAIL \
|
||||||
|
$GITEA_URL
|
||||||
|
|
||||||
|
new_fleet_repo_init $GITEA_HOSTEA_FLEET_REPO
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
# Create user on Hostea to simulate a Hostea customer
|
# Create user on Hostea to simulate a Hostea customer
|
||||||
hostea_customer_simulation() {
|
hostea_customer_simulation() {
|
||||||
python -m integration \
|
python -m integration \
|
||||||
|
|
|
@ -37,6 +37,7 @@ pynvim==0.4.3
|
||||||
pytz==2022.1
|
pytz==2022.1
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
requests==2.27.1
|
requests==2.27.1
|
||||||
|
six==1.16.0
|
||||||
smmap==5.0.0
|
smmap==5.0.0
|
||||||
sqlparse==0.4.2
|
sqlparse==0.4.2
|
||||||
stripe==3.4.0
|
stripe==3.4.0
|
||||||
|
|
|
@ -34,11 +34,9 @@ h2 {
|
||||||
body {
|
body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
/*
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
|
@ -244,6 +242,8 @@ footer {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
margin-left: 260px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer__container {
|
.footer__container {
|
||||||
|
@ -391,6 +391,8 @@ footer {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin-left: 260px;
|
||||||
|
width: calc(100vw - 260px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer__container {
|
.footer__container {
|
||||||
|
@ -607,8 +609,6 @@ fieldset {
|
||||||
background-color: #e11d21;
|
background-color: #e11d21;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
.form__label {
|
.form__label {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
from dash.utils import footer_ctx
|
||||||
from .utils import IssueTracker
|
from .utils import IssueTracker
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ def default_ctx(title: str, username: str):
|
||||||
"username": username,
|
"username": username,
|
||||||
"open_support": "open",
|
"open_support": "open",
|
||||||
"support": {"list": it.get_issue_tracker(), "new": it.open_issue()},
|
"support": {"list": it.get_issue_tracker(), "new": it.open_issue()},
|
||||||
|
"footer": footer_ctx(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue